зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1600658: Part 2 - Move content crash test into a session test and modify TestCrashHandler to selectively evaluate crash report submission; r=snorp
* We add bidirectional messaging to `TestCrashHandler`: 1. The test sends a message to the `TestCrashHandler`, notifying it that an upcoming crash is intentional and its intent should be checked. 2. Upon receipt of the crash report, the service reviews the contents of the crash intent, and then sends a message back to the test with the test results. 3. The service deletes any crash dump artifacts belonging to crash intents that have been evaluated so that the harness doesn't pick up any intentional crashes. * We remove `crashContent` from `CrashTest.kt` and create `ContentCrashTest.kt` for that case. The `crashParent` test remains unchanged other than switching its crash handler over to `TestCrashHandler`. * We remove the `CrashTestHandler` service, as both `crashContent` and `crashParent` tests now use `TestCrashHandler`. Differential Revision: https://phabricator.services.mozilla.com/D56854 --HG-- rename : mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/crash/CrashTest.kt => mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentCrashTest.kt rename : mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/crash/CrashTest.kt => mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/crash/ParentCrashTest.kt extra : moz-landing-system : lando
This commit is contained in:
Родитель
665493fd6a
Коммит
651331b493
|
@ -34,18 +34,13 @@
|
|||
android:process=":crash">
|
||||
</service>
|
||||
|
||||
<!-- These are needed for CrashTest -->
|
||||
<!-- This is needed for ParentCrashTest -->
|
||||
<service
|
||||
android:name=".crash.RemoteGeckoService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":gecko">
|
||||
</service>
|
||||
<service
|
||||
android:name=".crash.CrashTestHandler"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package org.mozilla.geckoview.test
|
||||
|
||||
import android.support.test.InstrumentationRegistry
|
||||
import android.support.test.filters.MediumTest
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
import android.util.Log
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.geckoview.BuildConfig
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.IgnoreCrash
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.Setting
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
|
||||
import org.mozilla.geckoview.test.util.Callbacks
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@MediumTest
|
||||
class ContentCrashTest : BaseSessionTest() {
|
||||
val client = TestCrashHandler.Client(InstrumentationRegistry.getTargetContext())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
assertTrue(client.connect(env.defaultTimeoutMillis))
|
||||
client.setEvalNextCrashDump(/* expectFatal */ false)
|
||||
}
|
||||
|
||||
@IgnoreCrash
|
||||
@Test
|
||||
fun crashContent() {
|
||||
assumeTrue(sessionRule.env.isMultiprocess)
|
||||
// We need the crash reporter for this test
|
||||
assumeTrue(BuildConfig.MOZ_CRASHREPORTER)
|
||||
|
||||
mainSession.loadUri(CONTENT_CRASH_URL)
|
||||
mainSession.waitUntilCalled(Callbacks.ContentDelegate::class, "onCrash")
|
||||
|
||||
// This test is really slow so we allow double the usual timeout
|
||||
var evalResult = client.getEvalResult(env.defaultTimeoutMillis * 2)
|
||||
assertTrue(evalResult.mMsg, evalResult.mResult)
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
client.disconnect()
|
||||
}
|
||||
}
|
|
@ -1,16 +1,250 @@
|
|||
package org.mozilla.geckoview.test;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mozilla.geckoview.GeckoRuntime;
|
||||
import org.mozilla.geckoview.test.util.UiThreadUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class TestCrashHandler extends Service {
|
||||
private static final int MSG_EVAL_NEXT_CRASH_DUMP = 1;
|
||||
private static final int MSG_CRASH_DUMP_EVAL_RESULT = 2;
|
||||
private static final String LOGTAG = "TestCrashHandler";
|
||||
|
||||
public static final class EvalResult {
|
||||
private static final String BUNDLE_KEY_RESULT = "TestCrashHandler.EvalResult.mResult";
|
||||
private static final String BUNDLE_KEY_MSG = "TestCrashHandler.EvalResult.mMsg";
|
||||
|
||||
public EvalResult(boolean result, String msg) {
|
||||
mResult = result;
|
||||
mMsg = msg;
|
||||
}
|
||||
|
||||
public EvalResult(Bundle bundle) {
|
||||
mResult = bundle.getBoolean(BUNDLE_KEY_RESULT, false);
|
||||
mMsg = bundle.getString(BUNDLE_KEY_MSG);
|
||||
}
|
||||
|
||||
public Bundle asBundle() {
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(BUNDLE_KEY_RESULT, mResult);
|
||||
bundle.putString(BUNDLE_KEY_MSG, mMsg);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
public boolean mResult;
|
||||
public String mMsg;
|
||||
}
|
||||
|
||||
public static final class Client {
|
||||
private static final String LOGTAG = "TestCrashHandler.Client";
|
||||
|
||||
private class Receiver extends Handler {
|
||||
public Receiver(final Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (msg.what == MSG_CRASH_DUMP_EVAL_RESULT) {
|
||||
setEvalResult(new EvalResult(msg.getData()));
|
||||
return;
|
||||
}
|
||||
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private Receiver mReceiver;
|
||||
private boolean mDoUnbind = false;
|
||||
private Messenger mService = null;
|
||||
private Messenger mMessenger;
|
||||
private Context mContext;
|
||||
private HandlerThread mThread;
|
||||
private EvalResult mResult = null;
|
||||
|
||||
private ServiceConnection mConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
mService = new Messenger(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
mService = null;
|
||||
}
|
||||
};
|
||||
|
||||
public Client(final Context context) {
|
||||
mContext = context;
|
||||
mThread = new HandlerThread("TestCrashHandler.Client");
|
||||
mThread.start();
|
||||
mReceiver = new Receiver(mThread.getLooper());
|
||||
mMessenger = new Messenger(mReceiver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests should call this to notify the crash handler that the next crash it sees is
|
||||
* intentional and that its intent should be checked for correctness.
|
||||
*
|
||||
* @param expectFatal Whether the incoming crash is expected to be fatal or not.
|
||||
*/
|
||||
public void setEvalNextCrashDump(final boolean expectFatal) {
|
||||
setEvalResult(null);
|
||||
mReceiver.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Message msg = Message.obtain(null, MSG_EVAL_NEXT_CRASH_DUMP,
|
||||
expectFatal ? 1 : 0, 0);
|
||||
msg.replyTo = mMessenger;
|
||||
|
||||
try {
|
||||
mService.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean connect(final long timeoutMillis) {
|
||||
Intent intent = new Intent(mContext, TestCrashHandler.class);
|
||||
mDoUnbind = mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE |
|
||||
Context.BIND_IMPORTANT);
|
||||
if (!mDoUnbind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UiThreadUtils.waitForCondition(() -> mService != null, timeoutMillis);
|
||||
|
||||
return mService != null;
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
if (mDoUnbind) {
|
||||
mContext.unbindService(mConnection);
|
||||
}
|
||||
mThread.quitSafely();
|
||||
}
|
||||
|
||||
private synchronized void setEvalResult(EvalResult result) {
|
||||
mResult = result;
|
||||
}
|
||||
|
||||
private synchronized EvalResult getEvalResult() {
|
||||
return mResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests should call this method after initiating the intentional crash to wait for the
|
||||
* result from the crash handler.
|
||||
*
|
||||
* @param timeoutMillis timeout in milliseconds
|
||||
* @return EvalResult containing the boolean result of the test and an error message.
|
||||
*/
|
||||
public EvalResult getEvalResult(final long timeoutMillis) {
|
||||
UiThreadUtils.waitForCondition(() -> getEvalResult() != null, timeoutMillis);
|
||||
return getEvalResult();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MessageHandler extends Handler {
|
||||
private Messenger mReplyToMessenger;
|
||||
private boolean mExpectFatal = false;
|
||||
|
||||
MessageHandler() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (msg.what == MSG_EVAL_NEXT_CRASH_DUMP) {
|
||||
mReplyToMessenger = msg.replyTo;
|
||||
mExpectFatal = msg.arg1 != 0;
|
||||
return;
|
||||
}
|
||||
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
|
||||
public void reportResult(EvalResult result) {
|
||||
if (mReplyToMessenger == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Message msg = Message.obtain(null, MSG_CRASH_DUMP_EVAL_RESULT);
|
||||
msg.setData(result.asBundle());
|
||||
|
||||
try {
|
||||
mReplyToMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
|
||||
mReplyToMessenger = null;
|
||||
}
|
||||
|
||||
public boolean getExpectFatal() {
|
||||
return mExpectFatal;
|
||||
}
|
||||
}
|
||||
|
||||
private Messenger mMessenger;
|
||||
private MessageHandler mMsgHandler;
|
||||
|
||||
public TestCrashHandler() {
|
||||
}
|
||||
|
||||
private EvalResult evalCrashInfo(final Intent intent) {
|
||||
if (!intent.getAction().equals(GeckoRuntime.ACTION_CRASHED)) {
|
||||
return new EvalResult(false, "Action should match");
|
||||
}
|
||||
|
||||
final File dumpFile = new File(intent.getStringExtra(GeckoRuntime.EXTRA_MINIDUMP_PATH));
|
||||
final boolean dumpFileExists = dumpFile.exists();
|
||||
dumpFile.delete();
|
||||
|
||||
final File extrasFile = new File(intent.getStringExtra(GeckoRuntime.EXTRA_EXTRAS_PATH));
|
||||
final boolean extrasFileExists = extrasFile.exists();
|
||||
extrasFile.delete();
|
||||
|
||||
if (!dumpFileExists) {
|
||||
return new EvalResult(false, "Dump file should exist");
|
||||
}
|
||||
|
||||
if (!extrasFileExists) {
|
||||
return new EvalResult(false, "Extras file should exist");
|
||||
}
|
||||
|
||||
final boolean expectFatal = mMsgHandler.getExpectFatal();
|
||||
if (intent.getBooleanExtra(GeckoRuntime.EXTRA_CRASH_FATAL, !expectFatal) != expectFatal) {
|
||||
return new EvalResult(false, "Fatality should match");
|
||||
}
|
||||
|
||||
return new EvalResult(true, "Crash Dump OK");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
public synchronized int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (mMsgHandler != null) {
|
||||
mMsgHandler.reportResult(evalCrashInfo(intent));
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// We don't want to do anything, this handler only exists
|
||||
// so we produce a crash dump which is picked up by the
|
||||
// test harness.
|
||||
|
@ -18,9 +252,17 @@ public class TestCrashHandler extends Service {
|
|||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
public synchronized IBinder onBind(Intent intent) {
|
||||
mMsgHandler = new MessageHandler();
|
||||
mMessenger = new Messenger(mMsgHandler);
|
||||
return mMessenger.getBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean onUnbind(Intent intent) {
|
||||
mMsgHandler = null;
|
||||
mMessenger = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
package org.mozilla.geckoview.test.crash
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import java.util.concurrent.SynchronousQueue
|
||||
|
||||
class CrashTestHandler : Service() {
|
||||
companion object {
|
||||
val LOGTAG = "CrashTestHandler"
|
||||
val queue = SynchronousQueue<Intent>()
|
||||
}
|
||||
|
||||
var notification: Notification? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && notification == null) {
|
||||
val channelId = "test"
|
||||
val channel = NotificationChannel(channelId, "Crashes", NotificationManager.IMPORTANCE_LOW)
|
||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||
notificationManager!!.createNotificationChannel(channel)
|
||||
|
||||
notification = NotificationCompat.Builder(this, channelId)
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setContentTitle("Crash Test Service")
|
||||
.setContentText("I'm just here so I don't get killed")
|
||||
.setDefaults(Notification.DEFAULT_ALL)
|
||||
.setAutoCancel(true)
|
||||
.setOngoing(false)
|
||||
.build()
|
||||
|
||||
startForeground(42, notification)
|
||||
}
|
||||
|
||||
queue.put(intent)
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -9,7 +9,9 @@ import android.support.test.rule.ServiceTestRule
|
|||
import android.support.test.runner.AndroidJUnit4
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.hamcrest.Matchers.notNullValue
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertThat
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assume
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
|
@ -17,57 +19,42 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.mozilla.geckoview.BuildConfig
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
import org.mozilla.geckoview.test.TestCrashHandler
|
||||
import org.mozilla.geckoview.test.util.Environment
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@MediumTest
|
||||
class CrashTest {
|
||||
class ParentCrashTest {
|
||||
lateinit var messenger: Messenger
|
||||
val env = Environment()
|
||||
|
||||
@get:Rule val rule = ServiceTestRule()
|
||||
|
||||
val client = TestCrashHandler.Client(InstrumentationRegistry.getTargetContext())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
CrashTestHandler.queue.clear()
|
||||
|
||||
val context = InstrumentationRegistry.getTargetContext()
|
||||
val binder = rule.bindService(Intent(context, RemoteGeckoService::class.java))
|
||||
messenger = Messenger(binder)
|
||||
assertThat("messenger should not be null", binder, notNullValue())
|
||||
}
|
||||
|
||||
fun assertCrashIntent(intent: Intent, fatal: Boolean) {
|
||||
assertThat("Action should match",
|
||||
intent.action, equalTo(GeckoRuntime.ACTION_CRASHED))
|
||||
assertThat("Dump file should exist",
|
||||
File(intent.getStringExtra(GeckoRuntime.EXTRA_MINIDUMP_PATH)).exists(),
|
||||
equalTo(true))
|
||||
assertThat("Extras file should exist",
|
||||
File(intent.getStringExtra(GeckoRuntime.EXTRA_EXTRAS_PATH)).exists(),
|
||||
equalTo(true))
|
||||
|
||||
assertThat("Fatality should match",
|
||||
intent.getBooleanExtra(GeckoRuntime.EXTRA_CRASH_FATAL, !fatal), equalTo(fatal))
|
||||
assertTrue(client.connect(env.defaultTimeoutMillis))
|
||||
client.setEvalNextCrashDump(/* expectFatal */ true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashParent() {
|
||||
messenger.send(Message.obtain(null, RemoteGeckoService.CMD_CRASH_PARENT_NATIVE))
|
||||
assertCrashIntent(CrashTestHandler.queue.take(), true)
|
||||
|
||||
var evalResult = client.getEvalResult(env.defaultTimeoutMillis)
|
||||
assertTrue(evalResult.mMsg, evalResult.mResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashContent() {
|
||||
// We need the crash reporter for this test
|
||||
Assume.assumeTrue(BuildConfig.MOZ_CRASHREPORTER)
|
||||
|
||||
messenger.send(Message.obtain(null, RemoteGeckoService.CMD_CRASH_CONTENT_NATIVE))
|
||||
|
||||
// This test is really slow so we allow double the usual timeout
|
||||
assertCrashIntent(CrashTestHandler.queue.poll(
|
||||
env.defaultTimeoutMillis * 2, TimeUnit.MILLISECONDS), false)
|
||||
@After
|
||||
fun teardown() {
|
||||
client.disconnect()
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import org.mozilla.geckoview.GeckoRuntime
|
|||
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||
import org.mozilla.geckoview.GeckoSession
|
||||
import org.mozilla.geckoview.GeckoSessionSettings
|
||||
import org.mozilla.geckoview.test.TestCrashHandler
|
||||
|
||||
class RemoteGeckoService : Service() {
|
||||
companion object {
|
||||
|
@ -57,7 +58,7 @@ class RemoteGeckoService : Service() {
|
|||
runtime = GeckoRuntime.create(this.applicationContext,
|
||||
GeckoRuntimeSettings.Builder()
|
||||
.extras(extras)
|
||||
.crashHandler(CrashTestHandler::class.java).build())
|
||||
.crashHandler(TestCrashHandler::class.java).build())
|
||||
}
|
||||
|
||||
return handler.binder
|
||||
|
|
Загрузка…
Ссылка в новой задаче