Bug 1465480 - Add @IgnoreCrash to GeckoSessionTestRule r=jchen draft
authorJames Willcox <snorp@snorp.net>
Thu, 31 May 2018 10:28:34 -0500
changeset 804892 a4091f3021053e2c992959a0b35b26f1c34fd339
parent 804891 28cb36003be4862468c891365f6941b33ee26ac8
child 804893 f997e7c7168d674d0c009fde87512899ce3e605b
push id112488
push userbmo:snorp@snorp.net
push dateWed, 06 Jun 2018 17:59:13 +0000
reviewersjchen
bugs1465480
milestone62.0a1
Bug 1465480 - Add @IgnoreCrash to GeckoSessionTestRule r=jchen This allows a test to declare that crashes should not be considered fatal. It does not, however, assert that a crash will occur. That responsibility is left with the test itself. MozReview-Commit-ID: 4DB6xs5jlEZ
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
@@ -21,16 +21,17 @@ import kotlin.reflect.KClass
 
 /**
  * Common base class for tests using GeckoSessionTestRule,
  * providing the test rule and other utilities.
  */
 open class BaseSessionTest(noErrorCollector: Boolean = false) {
     companion object {
         const val CLICK_TO_RELOAD_HTML_PATH = "/assets/www/clickToReload.html"
+        const val CONTENT_CRASH_URL = "about:crashcontent"
         const val DOWNLOAD_HTML_PATH = "/assets/www/download.html"
         const val HELLO_HTML_PATH = "/assets/www/hello.html"
         const val HELLO2_HTML_PATH = "/assets/www/hello2.html"
         const val INVALID_URI = "http://www.test.invalid/"
         const val NEW_SESSION_HTML_PATH = "/assets/www/newSession.html"
         const val NEW_SESSION_CHILD_HTML_PATH = "/assets/www/newSession_child.html"
         const val SELECTION_ACTION_PATH = "/assets/www/selectionAction.html"
         const val TITLE_CHANGE_HTML_PATH = "/assets/www/titleChange.html"
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
@@ -3,18 +3,20 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
 import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.GeckoSessionSettings
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ClosedSessionAtStart
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.IgnoreCrash
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.NullDelegate
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.RejectedPromiseException
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.Setting
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.TimeoutException
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.TimeoutMillis
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.util.Callbacks
 
 import android.support.test.filters.MediumTest
@@ -1599,36 +1601,57 @@ class GeckoSessionTestRuleTest : BaseSes
     }
 
     @Test fun addExternalDelegateDuringNextWait_hasPrecedence() {
         var delegate: TestDelegate? = null
         val register = { newDelegate: TestDelegate -> delegate = newDelegate }
         val unregister = { _: TestDelegate -> delegate = null }
 
         sessionRule.addExternalDelegateDuringNextWait(TestDelegate::class, register, unregister,
-                                                      object : TestDelegate {
-            @AssertCalled(count = 1)
-            override fun onDelegate(foo: String, bar: String): Int {
-                return 24
-            }
-        })
+                object : TestDelegate {
+                    @AssertCalled(count = 1)
+                    override fun onDelegate(foo: String, bar: String): Int {
+                        return 24
+                    }
+                })
 
         sessionRule.addExternalDelegateUntilTestEnd(TestDelegate::class, register, unregister,
-                                                    object : TestDelegate {
-            @AssertCalled(count = 1)
-            override fun onDelegate(foo: String, bar: String): Int {
-                return 42
-            }
-        })
+                object : TestDelegate {
+                    @AssertCalled(count = 1)
+                    override fun onDelegate(foo: String, bar: String): Int {
+                        return 42
+                    }
+                })
 
         assertThat("Wait delegate should be registered", delegate, notNullValue())
         assertThat("Wait delegate return value should be correct",
-                   delegate?.onDelegate("", ""), equalTo(24))
+                delegate?.onDelegate("", ""), equalTo(24))
 
         mainSession.reload()
         mainSession.waitForPageStop()
 
         assertThat("Test delegate should still be registered", delegate, notNullValue())
         assertThat("Test delegate return value should be correct",
-                   delegate?.onDelegate("", ""), equalTo(42))
+                delegate?.onDelegate("", ""), equalTo(42))
         sessionRule.performTestEndCheck()
     }
+
+    @IgnoreCrash
+    @ReuseSession(false)
+    @Test fun contentCrashIgnored() {
+        assumeThat(sessionRule.env.isMultiprocess, equalTo(true))
+
+        sessionRule.session.loadUri(CONTENT_CRASH_URL)
+        sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
+            @AssertCalled(count = 1)
+            override fun onCrash(session: GeckoSession) = Unit
+        })
+    }
+
+    @Test(expected = RuntimeException::class)
+    @ReuseSession(false)
+    fun contentCrashFails() {
+        assumeThat(sessionRule.env.isMultiprocess, equalTo(true))
+
+        sessionRule.session.loadUri(CONTENT_CRASH_URL)
+        sessionRule.waitForPageStop()
+    }
 }
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
@@ -92,25 +92,28 @@ public class GeckoSessionTestRule extend
     private static final long DEFAULT_X86_EMULATOR_TIMEOUT_MILLIS = 5000;
     private static final long DEFAULT_IDE_DEBUG_TIMEOUT_MILLIS = 86400000;
 
     public static final String APK_URI_PREFIX = "resource://android/";
 
     private static final Method sGetNextMessage;
     private static final Method sOnPageStop;
     private static final Method sOnNewSession;
+    private static final Method sOnCrash;
 
     static {
         try {
             sGetNextMessage = MessageQueue.class.getDeclaredMethod("next");
             sGetNextMessage.setAccessible(true);
             sOnPageStop = GeckoSession.ProgressDelegate.class.getMethod(
                     "onPageStop", GeckoSession.class, boolean.class);
             sOnNewSession = GeckoSession.NavigationDelegate.class.getMethod(
                     "onNewSession", GeckoSession.class, String.class, GeckoResponse.class);
+            sOnCrash = GeckoSession.ContentDelegate.class.getMethod(
+                    "onCrash", GeckoSession.class);
         } catch (final NoSuchMethodException e) {
             throw new RuntimeException(e);
         }
     }
 
     /**
      * Specify the timeout for any of the wait methods, in milliseconds, relative to
      * {@link #DEFAULT_TIMEOUT_MILLIS}. When the default timeout scales to account
@@ -310,16 +313,30 @@ public class GeckoSessionTestRule extend
 
     /**
      * Interface that represents a function that registers or unregisters a delegate.
      */
     public interface DelegateRegistrar<T> {
         void invoke(T delegate) throws Throwable;
     }
 
+    /*
+     * If the value here is true, content crashes will be ignored. If false, the test will
+     * be failed immediately if a content crash occurs. This is also the case when
+     * {@link IgnoreCrash} is not present.
+     */
+    @Target(ElementType.METHOD)
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface IgnoreCrash {
+        /**
+         * @return True if content crashes should be ignored, false otherwise. Default is true.
+         */
+        boolean value() default true;
+    }
+
     public static class TimeoutException extends RuntimeException {
         public TimeoutException(final String detailMessage) {
             super(detailMessage);
         }
     }
 
     public static class RejectedPromiseException extends RuntimeException {
         private final Object mReason;
@@ -899,16 +916,17 @@ public class GeckoSessionTestRule extend
     protected SurfaceTexture mDisplayTexture;
     protected Surface mDisplaySurface;
     protected GeckoDisplay mDisplay;
     protected boolean mClosedSession;
     protected boolean mWithDevTools;
     protected Map<GeckoSession, Tab> mRDPTabs;
     protected Tab mRDPChromeProcess;
     protected boolean mReuseSession;
+    protected boolean mIgnoreCrash;
 
     public GeckoSessionTestRule() {
         mDefaultSettings = new GeckoSessionSettings();
         mDefaultSettings.setBoolean(GeckoSessionSettings.USE_MULTIPROCESS, env.isMultiprocess());
     }
 
     /**
      * Set an ErrorCollector for assertion errors, or null to not use one.
@@ -1071,16 +1089,18 @@ public class GeckoSessionTestRule extend
                 final WithDisplay displaySize = (WithDisplay)annotation;
                 mDisplaySize = new Point(displaySize.width(), displaySize.height());
             } else if (ClosedSessionAtStart.class.equals(annotation.annotationType())) {
                 mClosedSession = ((ClosedSessionAtStart) annotation).value();
             } else if (WithDevToolsAPI.class.equals(annotation.annotationType())) {
                 mWithDevTools = ((WithDevToolsAPI) annotation).value();
             } else if (ReuseSession.class.equals(annotation.annotationType())) {
                 mReuseSession = ((ReuseSession) annotation).value();
+            } else if (IgnoreCrash.class.equals(annotation.annotationType())) {
+                mIgnoreCrash = ((IgnoreCrash) annotation).value();
             }
         }
     }
 
     private static RuntimeException unwrapRuntimeException(final Throwable e) {
         final Throwable cause = e.getCause();
         if (cause != null && cause instanceof RuntimeException) {
             return (RuntimeException) cause;
@@ -1139,18 +1159,23 @@ public class GeckoSessionTestRule extend
                         case "toString":
                             return "Call Recorder";
                     }
                     ignore = true;
                 } else if (mCallRecordHandler != null) {
                     ignore = mCallRecordHandler.handleCall(method, args);
                 }
 
+                if (!mIgnoreCrash && sOnCrash.equals(method)) {
+                    throw new RuntimeException("Content process crashed");
+                }
+
                 final boolean isExternalDelegate =
                         !DEFAULT_DELEGATES.contains(method.getDeclaringClass());
+
                 if (!ignore) {
                     assertThat("Callbacks must be on UI thread",
                                Looper.myLooper(), equalTo(Looper.getMainLooper()));
 
                     final GeckoSession session;
                     if (isExternalDelegate) {
                         session = null;
                     } else {