--- 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
@@ -1,27 +1,29 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* Any copyright is dedicated to the Public Domain.
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
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ClosedSessionAtStart
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.WithDisplay
import org.mozilla.geckoview.test.util.Callbacks
-import android.support.test.filters.LargeTest
import android.support.test.filters.MediumTest
import android.support.test.runner.AndroidJUnit4
import org.hamcrest.Matchers.*
+import org.junit.Assume.assumeThat
import org.junit.Test
import org.junit.runner.RunWith
/**
* Test for the GeckoSessionTestRule class, to ensure it properly sets up a session for
* each test, and to ensure it can properly wait for and assert delegate
* callbacks.
*/
@@ -30,17 +32,17 @@ import org.junit.runner.RunWith
class GeckoSessionTestRuleTest : BaseSessionTest(noErrorCollector = true) {
@Test fun getSession() {
assertThat("Can get session", sessionRule.session, notNullValue())
assertThat("Session is open",
sessionRule.session.isOpen, equalTo(true))
}
- @GeckoSessionTestRule.ClosedSessionAtStart
+ @ClosedSessionAtStart
@Test fun getSession_closedSession() {
assertThat("Session is closed", sessionRule.session.isOpen, equalTo(false))
}
@Setting.List(Setting(key = Setting.Key.USE_PRIVATE_MODE, value = "true"),
Setting(key = Setting.Key.DISPLAY_MODE, value = "DISPLAY_MODE_MINIMAL_UI"))
@Setting(key = Setting.Key.USE_TRACKING_PROTECTION, value = "true")
@Test fun settingsApplied() {
@@ -52,19 +54,18 @@ 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 = AssertionError::class)
+ @Test(expected = TimeoutException::class)
@TimeoutMillis(1000)
- @LargeTest
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) {
if (!ifce.isInterface || !ifce.simpleName.endsWith("Delegate")) {
@@ -788,20 +789,19 @@ 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 = AssertionError::class)
+ @Test(expected = TimeoutException::class)
@TimeoutMillis(1000)
- @LargeTest
- @GeckoSessionTestRule.ClosedSessionAtStart
+ @ClosedSessionAtStart
fun noPendingCallbacks_withSpecificSession() {
sessionRule.createOpenSession()
// Make sure we don't have unexpected pending callbacks after opening a session.
sessionRule.waitUntilCalled(object : Callbacks.All {})
}
@Test fun waitForPageStop_withSpecificSession() {
val newSession = sessionRule.createOpenSession()
@@ -1040,17 +1040,20 @@ class GeckoSessionTestRuleTest : BaseSes
newSession.loadTestPath(HELLO_HTML_PATH)
sessionRule.session.loadTestPath(HELLO_HTML_PATH)
sessionRule.waitForPageStops(2)
assertThat("Callback count should be correct", counter, equalTo(2))
}
- @GeckoSessionTestRule.WithDisplay(width = 10, height = 10)
+ @WithDisplay(width = 10, height = 10)
@Test fun synthesizeTap() {
+ // synthesizeTap is unreliable under e10s.
+ assumeThat(sessionRule.env.isMultiprocess, equalTo(false))
+
sessionRule.session.loadTestPath(CLICK_TO_RELOAD_HTML_PATH)
sessionRule.session.waitForPageStop()
sessionRule.session.synthesizeTap(5, 5)
sessionRule.session.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
@@ -8,17 +8,17 @@ package org.mozilla.geckoview.test.rule;
import org.mozilla.gecko.gfx.GeckoDisplay;
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.util.Callbacks;
import static org.hamcrest.Matchers.*;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.assertThat;
import org.hamcrest.Matcher;
import org.junit.rules.ErrorCollector;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import android.app.Instrumentation;
@@ -30,16 +30,17 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.UiThreadTestRule;
+import android.util.Log;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.Surface;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -65,16 +66,17 @@ import kotlin.reflect.KClass;
/**
* TestRule that, for each test, sets up a GeckoSession, runs the test on the UI thread,
* and tears down the GeckoSession at the end of the test. The rule also provides methods
* for waiting on particular callbacks to be called, and methods for asserting that
* callbacks are called in the proper order.
*/
public class GeckoSessionTestRule extends UiThreadTestRule {
+ private static final String LOGTAG = "GeckoSessionTestRule";
private static final long DEFAULT_TIMEOUT_MILLIS = 10000;
private static final long DEFAULT_DEBUG_TIMEOUT_MILLIS = 86400000;
public static final String APK_URI_PREFIX = "resource://android/";
private static final Method sOnLocationChange;
private static final Method sOnPageStop;
@@ -170,22 +172,21 @@ public class GeckoSessionTestRule extend
if (boolean.class.equals(mType) || Boolean.class.equals(mType)) {
settings.setBoolean((GeckoSessionSettings.Key<Boolean>) mKey,
Boolean.valueOf(value));
} else if (int.class.equals(mType) || Integer.class.equals(mType)) {
try {
settings.setInt((GeckoSessionSettings.Key<Integer>) mKey,
(Integer) GeckoSessionSettings.class.getField(value)
.get(null));
- return;
} catch (final NoSuchFieldException | IllegalAccessException |
ClassCastException e) {
+ settings.setInt((GeckoSessionSettings.Key<Integer>) mKey,
+ Integer.valueOf(value));
}
- settings.setInt((GeckoSessionSettings.Key<Integer>) mKey,
- Integer.valueOf(value));
} else if (String.class.equals(mType)) {
settings.setString((GeckoSessionSettings.Key<String>) mKey, value);
} else {
throw new IllegalArgumentException("Unsupported type: " +
mType.getSimpleName());
}
}
}
@@ -243,16 +244,22 @@ public class GeckoSessionTestRule extend
/**
* @return If called, the order number for each call, or 0 to allow arbitrary
* order. If order's length is more than count, extra elements are not used;
* if order's length is less than count, the last element is repeated.
*/
int[] order() default 0;
}
+ public static class TimeoutException extends RuntimeException {
+ public TimeoutException(final String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
public static class CallRequirement {
public final boolean allowed;
public final int count;
public final int[] order;
public CallRequirement(final boolean allowed, final int count, final int[] order) {
this.allowed = allowed;
this.count = count;
@@ -506,16 +513,17 @@ public class GeckoSessionTestRule extend
}
final HashSet<Class<?>> set = new HashSet<>(list);
return set.toArray(new Class<?>[set.size()]);
}
private static final List<Class<?>> CALLBACK_CLASSES = Arrays.asList(getCallbackClasses());
private static GeckoRuntime sRuntime;
+ private static long sLongestWait;
public final Environment env = new Environment();
protected final Instrumentation mInstrumentation =
InstrumentationRegistry.getInstrumentation();
protected final GeckoSessionSettings mDefaultSettings;
protected final Set<GeckoSession> mSubSessions = new HashSet<>();
@@ -561,55 +569,54 @@ public class GeckoSessionTestRule extend
/**
* Assert a condition with junit.Assert or an error collector.
*
* @param reason Reason string
* @param value Value to check
* @param matcher Matcher for checking the value
*/
- public <T> void assertThat(final String reason, final T value, final Matcher<T> matcher) {
+ public <T> void checkThat(final String reason, final T value, final Matcher<? super T> matcher) {
if (mErrorCollector != null) {
mErrorCollector.checkThat(reason, value, matcher);
} else {
- org.junit.Assert.assertThat(reason, value, matcher);
+ assertThat(reason, value, matcher);
}
}
private void assertAllowMoreCalls(final MethodCall call) {
final int count = call.getCount();
if (count != -1) {
- assertThat(call.method.getName() + " call count should be within limit",
- call.getCurrentCount() + 1, lessThanOrEqualTo(count));
+ checkThat(call.method.getName() + " call count should be within limit",
+ call.getCurrentCount() + 1, lessThanOrEqualTo(count));
}
}
private void assertOrder(final MethodCall call, final int order) {
final int newOrder = call.getOrder();
if (newOrder != 0) {
- assertThat(call.method.getName() + " should be in order",
- newOrder, greaterThanOrEqualTo(order));
+ checkThat(call.method.getName() + " should be in order",
+ newOrder, greaterThanOrEqualTo(order));
}
}
private void assertMatchesCount(final MethodCall call) {
if (call.requirement == null) {
return;
}
final int count = call.getCount();
if (count == 0) {
- assertThat(call.method.getName() + " should not be called",
- call.getCurrentCount(), equalTo(0));
+ checkThat(call.method.getName() + " should not be called",
+ call.getCurrentCount(), equalTo(0));
} else if (count == -1) {
- assertThat(call.method.getName() + " should be called",
- call.getCurrentCount(), greaterThan(0));
+ checkThat(call.method.getName() + " should be called",
+ call.getCurrentCount(), greaterThan(0));
} else {
- assertThat(call.method.getName() +
- " should be called specified number of times",
- call.getCurrentCount(), equalTo(count));
+ checkThat(call.method.getName() + " should be called specified number of times",
+ call.getCurrentCount(), equalTo(count));
}
}
/**
* Get the session set up for the current test.
*
* @return GeckoSession object.
*/
@@ -664,18 +671,18 @@ public class GeckoSessionTestRule extend
return (RuntimeException) e;
}
return new RuntimeException(cause != null ? cause : e);
}
protected void prepareStatement(final Description description) throws Throwable {
final GeckoSessionSettings settings = new GeckoSessionSettings(mDefaultSettings);
- mTimeoutMillis = !env.isDebugging() ? DEFAULT_TIMEOUT_MILLIS
- : DEFAULT_DEBUG_TIMEOUT_MILLIS;
+ mTimeoutMillis = env.isDebugging() ? DEFAULT_DEBUG_TIMEOUT_MILLIS
+ : DEFAULT_TIMEOUT_MILLIS;
mClosedSession = false;
applyAnnotations(Arrays.asList(description.getTestClass().getAnnotations()), settings);
applyAnnotations(description.getAnnotations(), settings);
final List<CallRecord> records = new ArrayList<>();
final CallbackDelegates waitDelegates = new CallbackDelegates();
final CallbackDelegates testDelegates = new CallbackDelegates();
@@ -697,16 +704,18 @@ public class GeckoSessionTestRule extend
} else if (mCallRecordHandler != null) {
ignore = mCallRecordHandler.handleCall(method, args);
}
if (!ignore) {
assertThat("Callbacks must be on UI thread",
Looper.myLooper(), equalTo(Looper.getMainLooper()));
assertThat("Callback first argument must be session object",
+ args, arrayWithSize(greaterThan(0)));
+ assertThat("Callback first argument must be session object",
args[0], instanceOf(GeckoSession.class));
final GeckoSession session = (GeckoSession) args[0];
records.add(new CallRecord(session, method, args));
call = waitDelegates.prepareMethodCall(session, method);
if (call == null) {
call = testDelegates.prepareMethodCall(session, method);
@@ -764,31 +773,22 @@ public class GeckoSessionTestRule extend
/**
* Call open() on a session, and ensure it's ready for use by the test. In particular,
* remove any extra calls recorded as part of opening the session.
*
* @param session Session to open.
*/
public void openSession(final GeckoSession session) {
- final boolean e10s = session.getSettings().getBoolean(
- GeckoSessionSettings.USE_MULTIPROCESS);
-
- if (e10s) {
- // Give any pending calls a chance to catch up.
- loopUntilIdle(/* timeout */ 0);
- }
+ session.open(sRuntime);
+ waitForInitialLoad(session);
+ }
- session.open(sRuntime);
-
- if (!e10s) {
- return;
- }
-
- // Under e10s, we receive an initial about:blank load; don't expose that to the test.
+ private void waitForInitialLoad(final GeckoSession session) {
+ // We receive an initial about:blank load; don't expose that to the test.
// The about:blank load is bounded by onLocationChange and onPageStop calls,
// so find the first about:blank onLocationChange, then the next onPageStop,
// and ignore everything in-between from that session.
try {
mCallRecordHandler = new CallRecordHandler() {
private boolean mFoundStart = false;
@@ -804,17 +804,17 @@ public class GeckoSessionTestRule extend
}
return true;
}
return false;
}
};
do {
- loopUntilIdle(mTimeoutMillis);
+ loopUntilIdle(DEFAULT_TIMEOUT_MILLIS);
} while (mCallRecordHandler != null);
} finally {
mCallRecordHandler = null;
}
}
/**
@@ -907,47 +907,54 @@ public class GeckoSessionTestRule extend
} catch (final NoSuchMethodException e) {
throw new RuntimeException(e);
}
getNextMessage.setAccessible(true);
final Runnable timeoutRunnable = new Runnable() {
@Override
public void run() {
- fail("Timed out after " + timeout + "ms");
+ throw new TimeoutException("Timed out after " + timeout + "ms");
}
};
if (timeout > 0) {
handler.postDelayed(timeoutRunnable, timeout);
} else {
queue.addIdleHandler(idleHandler);
}
+ final long startTime = SystemClock.uptimeMillis();
try {
while (true) {
final Message msg;
try {
msg = (Message) getNextMessage.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) {
looper.quit();
- break;
+ return;
}
msg.getTarget().dispatchMessage(msg);
if (timeout > 0) {
handler.removeCallbacks(timeoutRunnable);
queue.addIdleHandler(idleHandler);
}
}
+
+ final long waitDuration = SystemClock.uptimeMillis() - startTime;
+ if (waitDuration > sLongestWait) {
+ sLongestWait = waitDuration;
+ Log.i(LOGTAG, "New longest wait: " + waitDuration + "ms");
+ }
} finally {
if (timeout > 0) {
handler.removeCallbacks(timeoutRunnable);
}
}
}
/**
@@ -1136,26 +1143,26 @@ public class GeckoSessionTestRule extend
assertThat("Session should be wrapped through wrapSession",
session, isIn(mSubSessions));
}
// Make sure all handlers are set though #delegateUntilTestEnd or #delegateDuringNextWait,
// instead of through GeckoSession directly, so that we can still record calls even with
// custom handlers set.
for (final Class<?> ifce : CALLBACK_CLASSES) {
+ final Object callback;
try {
- assertThat("Callbacks should be set through" +
- " GeckoSessionTestRule delegate methods",
- getCallbackGetter(ifce).invoke(session == null ? mMainSession
- : session),
- sameInstance(mCallbackProxy));
+ callback = getCallbackGetter(ifce).invoke(session == null ? mMainSession : session);
} catch (final NoSuchMethodException | IllegalAccessException |
- InvocationTargetException e) {
+ InvocationTargetException e) {
throw unwrapRuntimeException(e);
}
+ assertThat(ifce.getSimpleName() + " callbacks should be " +
+ "accessed through GeckoSessionTestRule delegate methods",
+ callback, sameInstance(mCallbackProxy));
}
boolean calledAny = false;
int index = mLastWaitStart = mLastWaitEnd;
while (!calledAny || !methodCalls.isEmpty()) {
while (index >= mCallRecords.size()) {
loopUntilIdle(mTimeoutMillis);
@@ -1234,18 +1241,18 @@ public class GeckoSessionTestRule extend
for (int index = mLastWaitStart; index < mLastWaitEnd; index++) {
final CallRecord record = mCallRecords.get(index);
if (!record.method.getDeclaringClass().isInstance(callback) ||
(session != null && record.args[0] != session)) {
continue;
}
final int i = methodCalls.indexOf(record.methodCall);
- assertThat(record.method.getName() + " should be found",
- i, greaterThanOrEqualTo(0));
+ checkThat(record.method.getName() + " should be found",
+ i, greaterThanOrEqualTo(0));
final MethodCall methodCall = methodCalls.get(i);
assertAllowMoreCalls(methodCall);
methodCall.incrementCounter();
assertOrder(methodCall, order);
order = Math.max(methodCall.getOrder(), order);
try {
@@ -1261,19 +1268,19 @@ public class GeckoSessionTestRule extend
for (final MethodCall methodCall : methodCalls) {
assertMatchesCount(methodCall);
if (methodCall.requirement != null) {
calledAny = true;
}
}
- assertThat("Should have called one of " +
- Arrays.toString(callback.getClass().getInterfaces()),
- calledAny, equalTo(true));
+ checkThat("Should have called one of " +
+ Arrays.toString(callback.getClass().getInterfaces()),
+ calledAny, equalTo(true));
}
/**
* Get information about the current call. Only valid during a {@link
* #forCallbacksDuringWait}, {@link #delegateDuringNextWait}, or {@link
* #delegateUntilTestEnd} callback.
*
* @return Call information
@@ -1281,17 +1288,17 @@ public class GeckoSessionTestRule extend
public @NonNull CallInfo getCurrentCall() {
assertThat("Should be in a method call", mCurrentMethodCall, notNullValue());
return mCurrentMethodCall.getInfo();
}
/**
* Delegate implemented interfaces to the specified callback object for all sessions,
* for the rest of the test. Only GeckoSession callback interfaces are supported.
- * Delegates for {@link #delegateUntilTestEnd} can be temporarily overridden by
+ * Delegates for {@code delegateUntilTestEnd} can be temporarily overridden by
* delegates for {@link #delegateDuringNextWait}.
*
* @param callback Callback object, or null to clear all previously-set delegates.
*/
public void delegateUntilTestEnd(final @NonNull Object callback) {
delegateUntilTestEnd(/* session */ null, callback);
}
@@ -1307,17 +1314,17 @@ public class GeckoSessionTestRule extend
public void delegateUntilTestEnd(final @Nullable GeckoSession session,
final @NonNull Object callback) {
mTestScopeDelegates.delegate(session, callback);
}
/**
* Delegate implemented interfaces to the specified callback object for all sessions,
* during the next wait. Only GeckoSession callback interfaces are supported.
- * Delegates for {@link #delegateDuringNextWait} can temporarily take precedence over
+ * Delegates for {@code delegateDuringNextWait} can temporarily take precedence over
* delegates for {@link #delegateUntilTestEnd}.
*
* @param callback Callback object, or null to clear all previously-set delegates.
*/
public void delegateDuringNextWait(final @NonNull Object callback) {
delegateDuringNextWait(/* session */ null, callback);
}
@@ -1427,13 +1434,14 @@ public class GeckoSessionTestRule extend
* Asserts that {@code foo} is equal to {@code "bar"} during the first call and {@code
* "baz"} during the second call:
* <pre>{@code assertThat("Foo should match", foo, equalTo(forEachCall("bar",
* "baz")));}</pre>
*
* @param values Input array
* @return Value from input array indexed by the current call counter.
*/
- public <T> T forEachCall(T... values) {
+ @SafeVarargs
+ public final <T> T forEachCall(T... values) {
assertThat("Should be in a method call", mCurrentMethodCall, notNullValue());
return values[Math.min(mCurrentMethodCall.getCurrentCount(), values.length) - 1];
}
}