summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestCrashHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestCrashHandler.java')
-rw-r--r--mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestCrashHandler.java281
1 files changed, 281 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestCrashHandler.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestCrashHandler.java
new file mode 100644
index 0000000000..32917ac25b
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestCrashHandler.java
@@ -0,0 +1,281 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+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 java.io.File;
+import org.mozilla.geckoview.GeckoRuntime;
+import org.mozilla.geckoview.test.util.UiThreadUtils;
+
+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(final boolean result, final String msg) {
+ mResult = result;
+ mMsg = msg;
+ }
+
+ public EvalResult(final 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(final 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(final ComponentName className, final IBinder service) {
+ mService = new Messenger(service);
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName className) {
+ disconnect();
+ }
+ };
+
+ 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 expectedProcessType The type of process the incoming crash is expected to be for.
+ */
+ public void setEvalNextCrashDump(final String expectedProcessType) {
+ setEvalResult(null);
+ mReceiver.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ final Bundle bundle = new Bundle();
+ bundle.putString(GeckoRuntime.EXTRA_CRASH_PROCESS_TYPE, expectedProcessType);
+ final Message msg = Message.obtain(null, MSG_EVAL_NEXT_CRASH_DUMP, bundle);
+ msg.replyTo = mMessenger;
+
+ try {
+ mService.send(msg);
+ } catch (final RemoteException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+ });
+ }
+
+ public boolean connect(final long timeoutMillis) {
+ final 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);
+ mService = null;
+ mDoUnbind = false;
+ }
+ mThread.quitSafely();
+ }
+
+ private synchronized void setEvalResult(final 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 String mExpectedProcessType;
+
+ MessageHandler() {}
+
+ @Override
+ public void handleMessage(final Message msg) {
+ if (msg.what == MSG_EVAL_NEXT_CRASH_DUMP) {
+ mReplyToMessenger = msg.replyTo;
+ Bundle bundle = (Bundle) msg.obj;
+ mExpectedProcessType = bundle.getString(GeckoRuntime.EXTRA_CRASH_PROCESS_TYPE);
+ return;
+ }
+
+ super.handleMessage(msg);
+ }
+
+ public void reportResult(final EvalResult result) {
+ if (mReplyToMessenger == null) {
+ return;
+ }
+
+ final Message msg = Message.obtain(null, MSG_CRASH_DUMP_EVAL_RESULT);
+ msg.setData(result.asBundle());
+
+ try {
+ mReplyToMessenger.send(msg);
+ } catch (final RemoteException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+
+ mReplyToMessenger = null;
+ }
+
+ public String getExpectedProcessType() {
+ return mExpectedProcessType;
+ }
+ }
+
+ 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 String expectedProcessType = mMsgHandler.getExpectedProcessType();
+ final String processType = intent.getStringExtra(GeckoRuntime.EXTRA_CRASH_PROCESS_TYPE);
+ if (processType == null) {
+ return new EvalResult(false, "Intent missing process type");
+ }
+ if (!processType.equals(expectedProcessType)) {
+ return new EvalResult(
+ false, "Expected process type " + expectedProcessType + ", found " + processType);
+ }
+
+ return new EvalResult(true, "Crash Dump OK");
+ }
+
+ @Override
+ public synchronized int onStartCommand(final Intent intent, final int flags, final int startId) {
+ if (mMsgHandler != null) {
+ mMsgHandler.reportResult(evalCrashInfo(intent));
+ // We must manually call stopSelf() here to ensure the Service gets killed once the client
+ // unbinds. If we don't, then when the next client attempts to bind for a different test,
+ // onBind() will not be called, and mMsgHandler will not get set.
+ stopSelf();
+ 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.
+ System.exit(0);
+ return Service.START_NOT_STICKY;
+ }
+
+ @Override
+ public synchronized IBinder onBind(final Intent intent) {
+ mMsgHandler = new MessageHandler();
+ mMessenger = new Messenger(mMsgHandler);
+ return mMessenger.getBinder();
+ }
+
+ @Override
+ public synchronized boolean onUnbind(final Intent intent) {
+ mMsgHandler = null;
+ mMessenger = null;
+ return false;
+ }
+}