Bug 1468048 - Factor out loopUntilIdle() from GeckoSessionTestRule into UiThreadUtils r=jchen draft
authorJames Willcox <snorp@snorp.net>
Thu, 05 Jul 2018 14:45:27 -0500
changeset 814636 e6ca5e30ed54c2a8c61c115fbe79220b69be223f
parent 813685 1dc88ad547beba735d964f59e1b1d630c27d62f5
child 814637 ffefc8dbf983261247c9fbe923c7376c4c50b177
push id115291
push userbmo:snorp@snorp.net
push dateThu, 05 Jul 2018 20:44:33 +0000
reviewersjchen
bugs1468048
milestone63.0a1
Bug 1468048 - Factor out loopUntilIdle() from GeckoSessionTestRule into UiThreadUtils r=jchen This allows us to write other tests that need similar loop integration without having to use GeckoSessionTestRule. MozReview-Commit-ID: LfVSGhvyMoP
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/UiThreadUtils.java
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
--- 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
@@ -61,17 +61,17 @@ class GeckoSessionTestRuleTest : BaseSes
                    sessionRule.session.settings.getInt(GeckoSessionSettings.DISPLAY_MODE),
                    equalTo(GeckoSessionSettings.DISPLAY_MODE_MINIMAL_UI))
         assertThat("USE_TRACKING_PROTECTION should be set",
                    sessionRule.session.settings.getBoolean(
                            GeckoSessionSettings.USE_TRACKING_PROTECTION),
                    equalTo(true))
     }
 
-    @Test(expected = TimeoutException::class)
+    @Test(expected = UiThreadUtils.TimeoutException::class)
     @TimeoutMillis(1000)
     fun noPendingCallbacks() {
         // Make sure we don't have unexpected pending callbacks at the start of a test.
         sessionRule.waitUntilCalled(object : Callbacks.All {})
     }
 
     @Test fun includesAllCallbacks() {
         for (ifce in GeckoSession::class.java.classes) {
@@ -928,17 +928,17 @@ class GeckoSessionTestRuleTest : BaseSes
     @Test fun createClosedSession_withSettings() {
         val settings = GeckoSessionSettings(sessionRule.session.settings)
         settings.setBoolean(GeckoSessionSettings.USE_PRIVATE_MODE, true)
 
         val newSession = sessionRule.createClosedSession(settings)
         assertThat("New session has same settings", newSession.settings, equalTo(settings))
     }
 
-    @Test(expected = TimeoutException::class)
+    @Test(expected = UiThreadUtils.TimeoutException::class)
     @TimeoutMillis(1000)
     @ClosedSessionAtStart
     fun noPendingCallbacks_withSpecificSession() {
         sessionRule.createOpenSession()
         // Make sure we don't have unexpected pending callbacks after opening a session.
         sessionRule.waitUntilCalled(object : Callbacks.All {})
     }
 
@@ -1323,17 +1323,17 @@ class GeckoSessionTestRuleTest : BaseSes
         // by calling alert(), which blocks until prompt delegate is called.
         assertThat("JS blocking result should be correct",
                    sessionRule.session.evaluateJS("alert(); 'foo'") as String,
                    equalTo("foo"))
     }
 
     @WithDevToolsAPI
     @TimeoutMillis(1000)
-    @Test(expected = TimeoutException::class)
+    @Test(expected = UiThreadUtils.TimeoutException::class)
     fun evaluateJS_canTimeout() {
         sessionRule.session.delegateUntilTestEnd(object : Callbacks.PromptDelegate {
             override fun onAlert(session: GeckoSession, title: String, msg: String, callback: GeckoSession.PromptDelegate.AlertCallback) {
                 // Do nothing for the alert, so it hangs forever.
             }
         })
         sessionRule.session.evaluateJS("alert()")
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/UiThreadUtils.java
@@ -0,0 +1,108 @@
+package org.mozilla.geckoview.test;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class UiThreadUtils {
+    private static Method sGetNextMessage = null;
+    static {
+        try {
+            sGetNextMessage = MessageQueue.class.getDeclaredMethod("next");
+            sGetNextMessage.setAccessible(true);
+        } catch (NoSuchMethodException e) {
+        }
+    }
+
+    public static class TimeoutException extends RuntimeException {
+        public TimeoutException(final String detailMessage) {
+            super(detailMessage);
+        }
+    }
+
+    private static final class TimeoutRunnable implements Runnable {
+        private long timeout;
+
+        public void set(final long timeout) {
+            this.timeout = timeout;
+            cancel();
+            HANDLER.postDelayed(this, timeout);
+        }
+
+        public void cancel() {
+            HANDLER.removeCallbacks(this);
+        }
+
+        @Override
+        public void run() {
+            throw new TimeoutException("Timed out after " + timeout + "ms");
+        }
+    }
+
+    /* package */ static final Handler HANDLER = new Handler(Looper.getMainLooper());
+    private static final TimeoutRunnable TIMEOUT_RUNNABLE = new TimeoutRunnable();
+    private static final MessageQueue.IdleHandler IDLE_HANDLER = new MessageQueue.IdleHandler() {
+        @Override
+        public boolean queueIdle() {
+            final Message msg = Message.obtain(HANDLER);
+            msg.obj = HANDLER;
+            HANDLER.sendMessageAtFrontOfQueue(msg);
+            return false; // Remove this idle handler.
+        }
+    };
+
+    public static RuntimeException unwrapRuntimeException(final Throwable e) {
+        final Throwable cause = e.getCause();
+        if (cause != null && cause instanceof RuntimeException) {
+            return (RuntimeException) cause;
+        } else if (e instanceof RuntimeException) {
+            return (RuntimeException) e;
+        }
+
+        return new RuntimeException(cause != null ? cause : e);
+    }
+
+    public static void loopUntilIdle(final long timeout) {
+        // Adapted from GeckoThread.pumpMessageLoop.
+        final MessageQueue queue = HANDLER.getLooper().getQueue();
+        if (timeout > 0) {
+            TIMEOUT_RUNNABLE.set(timeout);
+        } else {
+            queue.addIdleHandler(IDLE_HANDLER);
+        }
+
+        final long startTime = SystemClock.uptimeMillis();
+        try {
+            while (true) {
+                final Message msg;
+                try {
+                    msg = (Message) sGetNextMessage.invoke(queue);
+                } catch (final IllegalAccessException | InvocationTargetException e) {
+                    throw unwrapRuntimeException(e);
+                }
+                if (msg.getTarget() == HANDLER && msg.obj == HANDLER) {
+                    // Our idle signal.
+                    break;
+                } else if (msg.getTarget() == null) {
+                    HANDLER.getLooper().quit();
+                    return;
+                }
+                msg.getTarget().dispatchMessage(msg);
+
+                if (timeout > 0) {
+                    TIMEOUT_RUNNABLE.cancel();
+                    queue.addIdleHandler(IDLE_HANDLER);
+                }
+            }
+        } finally {
+            if (timeout > 0) {
+                TIMEOUT_RUNNABLE.cancel();
+            }
+        }
+    }
+}
--- 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
@@ -8,16 +8,17 @@ package org.mozilla.geckoview.test.rule;
 import org.mozilla.gecko.gfx.GeckoDisplay;
 import org.mozilla.geckoview.BuildConfig;
 import org.mozilla.geckoview.GeckoResponse;
 import org.mozilla.geckoview.GeckoRuntime;
 import org.mozilla.geckoview.GeckoRuntimeSettings;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
 import org.mozilla.geckoview.SessionTextInput;
+import org.mozilla.geckoview.test.UiThreadUtils;
 import org.mozilla.geckoview.test.rdp.Actor;
 import org.mozilla.geckoview.test.rdp.Promise;
 import org.mozilla.geckoview.test.rdp.RDPConnection;
 import org.mozilla.geckoview.test.rdp.Tab;
 import org.mozilla.geckoview.test.util.Callbacks;
 
 import static org.hamcrest.Matchers.*;
 import static org.junit.Assert.assertThat;
@@ -42,16 +43,17 @@ import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.MessageQueue;
 import android.os.Process;
 import android.os.SystemClock;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
 import android.support.test.rule.UiThreadTestRule;
 import android.util.Log;
 import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.Surface;
 
 import java.io.File;
 import java.lang.annotation.Annotation;
@@ -332,22 +334,16 @@ public class GeckoSessionTestRule extend
     @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 ChildCrashedException extends RuntimeException {
         public ChildCrashedException(final String detailMessage) {
             super(detailMessage);
         }
     }
 
     public static class RejectedPromiseException extends RuntimeException {
         private final Object mReason;
@@ -398,17 +394,17 @@ public class GeckoSessionTestRule extend
         /**
          * Wait for this promise to settle. If the promise is fulfilled, return its value.
          * If the promise is rejected, throw an exception containing the reason.
          *
          * @return Fulfilled value of the promise.
          */
         public Object getValue() {
             while (mPromise.isPending()) {
-                loopUntilIdle(mTimeoutMillis);
+                UiThreadUtils.loopUntilIdle(mTimeoutMillis);
             }
             if (mPromise.isRejected()) {
                 throw new RejectedPromiseException(mPromise.getReason());
             }
             return mPromise.getValue();
         }
     }
 
@@ -865,47 +861,16 @@ public class GeckoSessionTestRule extend
             addCallbackClasses(list, ifce);
         }
 
         return new HashSet<>(list);
     }
 
     private static final Set<Class<?>> DEFAULT_DELEGATES = getDefaultDelegates();
 
-    private static final class TimeoutRunnable implements Runnable {
-        private long timeout;
-
-        public void set(final long timeout) {
-            this.timeout = timeout;
-            cancel();
-            HANDLER.postDelayed(this, timeout);
-        }
-
-        public void cancel() {
-            HANDLER.removeCallbacks(this);
-        }
-
-        @Override
-        public void run() {
-            throw new TimeoutException("Timed out after " + timeout + "ms");
-        }
-    }
-
-    /* package */ static final Handler HANDLER = new Handler(Looper.getMainLooper());
-    private static final TimeoutRunnable TIMEOUT_RUNNABLE = new TimeoutRunnable();
-    private static final MessageQueue.IdleHandler IDLE_HANDLER = new MessageQueue.IdleHandler() {
-        @Override
-        public boolean queueIdle() {
-            final Message msg = Message.obtain(HANDLER);
-            msg.obj = HANDLER;
-            HANDLER.sendMessageAtFrontOfQueue(msg);
-            return false; // Remove this idle handler.
-        }
-    };
-
     private static GeckoRuntime sRuntime;
     private static RDPConnection sRDPConnection;
     private static long sLongestWait;
     protected static GeckoSession sCachedSession;
     protected static Tab sCachedRDPTab;
 
     public final Environment env = new Environment();
 
@@ -1385,32 +1350,32 @@ public class GeckoSessionTestRule extend
                     } else if (matching && mIsAboutBlank && sOnPageStop.equals(method)) {
                         mCallRecordHandler = null;
                     }
                     return matching;
                 }
             };
 
             do {
-                loopUntilIdle(getDefaultTimeoutMillis());
+                UiThreadUtils.loopUntilIdle(getDefaultTimeoutMillis());
             } while (mCallRecordHandler != null);
 
         } finally {
             mCallRecordHandler = null;
         }
     }
 
     /**
      * Internal method to perform callback checks at the end of a test.
      */
     public void performTestEndCheck() {
         if (sCachedSession != null && mIgnoreCrash) {
             // Make sure the cached session has been closed by crashes.
             while (sCachedSession.isOpen()) {
-                loopUntilIdle(mTimeoutMillis);
+                UiThreadUtils.loopUntilIdle(mTimeoutMillis);
             }
         }
 
         mWaitScopeDelegates.clearAndAssert();
         mTestScopeDelegates.clearAndAssert();
 
         if (sCachedSession != null && mReuseSession) {
             assertThat("Cached session should be open",
@@ -1504,69 +1469,16 @@ public class GeckoSessionTestRule extend
     }
 
     @Override
     protected boolean shouldRunOnUiThread(final Description description) {
         return true;
     }
 
     /**
-     * Loop the current thread until the message queue is idle. If loop is already idle and
-     * timeout is not specified, return immediately. If loop is already idle and timeout is
-     * specified, wait for a message to arrive first; an exception is thrown if timeout
-     * expires during the wait.
-     *
-     * @param timeout Wait timeout in milliseconds or 0 to not wait.
-     */
-    protected static void loopUntilIdle(final long timeout) {
-        // Adapted from GeckoThread.pumpMessageLoop.
-        final MessageQueue queue = HANDLER.getLooper().getQueue();
-        if (timeout > 0) {
-            TIMEOUT_RUNNABLE.set(timeout);
-        } else {
-            queue.addIdleHandler(IDLE_HANDLER);
-        }
-
-        final long startTime = SystemClock.uptimeMillis();
-        try {
-            while (true) {
-                final Message msg;
-                try {
-                    msg = (Message) sGetNextMessage.invoke(queue);
-                } catch (final IllegalAccessException | InvocationTargetException e) {
-                    throw unwrapRuntimeException(e);
-                }
-                if (msg.getTarget() == HANDLER && msg.obj == HANDLER) {
-                    // Our idle signal.
-                    break;
-                } else if (msg.getTarget() == null) {
-                    HANDLER.getLooper().quit();
-                    return;
-                }
-                msg.getTarget().dispatchMessage(msg);
-
-                if (timeout > 0) {
-                    TIMEOUT_RUNNABLE.cancel();
-                    queue.addIdleHandler(IDLE_HANDLER);
-                }
-            }
-
-            final long waitDuration = SystemClock.uptimeMillis() - startTime;
-            if (waitDuration > sLongestWait) {
-                sLongestWait = waitDuration;
-                Log.i(LOGTAG, "New longest wait: " + waitDuration + "ms");
-            }
-        } finally {
-            if (timeout > 0) {
-                TIMEOUT_RUNNABLE.cancel();
-            }
-        }
-    }
-
-    /**
      * Wait until a page load has finished on any session. A session must have started a
      * page load since the last wait, or this method will wait indefinitely.
      */
     public void waitForPageStop() {
         waitForPageStop(/* session */ null);
     }
 
     /**
@@ -1791,17 +1703,17 @@ public class GeckoSessionTestRule extend
         }
 
         boolean calledAny = false;
         int index = mLastWaitEnd;
         beforeWait();
 
         while (!calledAny || !methodCalls.isEmpty()) {
             while (index >= mCallRecords.size()) {
-                loopUntilIdle(mTimeoutMillis);
+                UiThreadUtils.loopUntilIdle(mTimeoutMillis);
             }
 
             final MethodCall recorded = mCallRecords.get(index).methodCall;
             calledAny |= recorded.method.getDeclaringClass().isAssignableFrom(delegate);
             index++;
 
             final int i = methodCalls.indexOf(recorded);
             if (i < 0) {
@@ -2153,17 +2065,17 @@ public class GeckoSessionTestRule extend
             assertThat("Should have chrome process object",
                        mRDPChromeProcess, notNullValue());
         }
     }
 
     private Object evaluateJS(final @NonNull Tab tab, final @NonNull String js) {
         final Actor.Reply<Object> reply = tab.getConsole().evaluateJS(js);
         while (!reply.hasResult()) {
-            loopUntilIdle(mTimeoutMillis);
+            UiThreadUtils.loopUntilIdle(mTimeoutMillis);
         }
 
         final Object result = reply.get();
         if (result instanceof Promise) {
             // Map the static Promise into a live Promise. In order to perform the mapping, we set
             // a tag on the static Promise, fetch a list of live Promises, and see which live
             // Promise has the same tag on it.
             final String tag = String.valueOf(result.hashCode());