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
--- 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());