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:
Aaron Klotz 2019-12-16 20:15:22 +00:00
Родитель 665493fd6a
Коммит 651331b493
6 изменённых файлов: 314 добавлений и 86 удалений

Просмотреть файл

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