Bug 1456209 - 2. Miscellaneous GeckoSessionTestRule changes; r=jchen
authorJim Chen <nchen@mozilla.com>
Tue, 24 Apr 2018 10:13:35 -0400
changeset 468947 f6dd07476c05f9eca4b049218cec5d84d88455ba
parent 468946 65fb23b3c5e0137cc7c77f3716c38063db36d32d
child 468948 bb4f5395f6dcb81d7073b71e07faa30a594ea064
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjchen
bugs1456209
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1456209 - 2. Miscellaneous GeckoSessionTestRule changes; r=jchen Miscellaneous small fixes to GeckoSessionTestRule, including: * Make some internal assertions _not_ go through the error collector, because by not throwing an exception at the time of the assertion, we may end up with another, less clear exception down the road. * Make timeout throw a distinct TimeoutException, so that tests that expect an AssertionError would not mistakenly pass due to a timeout. * Use the default timeout value for operations internal to the test harness, so that internal operations are not affected when using a custom timeout value. * Print the longest actual wait duration to logcat, so that it's easier to adjust timeout values in the future. * Wait for initial about:blank load for non-e10s as well, due to recent changes in GV startup code. MozReview-Commit-ID: KYJyGlK1yGF
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
@@ -32,17 +32,17 @@ open class BaseSessionTest(noErrorCollec
         const val CLICK_TO_RELOAD_HTML_PATH = "/assets/www/clickToReload.html"
         const val TITLE_CHANGE_HTML_PATH = "/assets/www/titleChange.html"
         const val DOWNLOAD_HTML_PATH = "/assets/www/download.html"
     }
 
     @get:Rule val sessionRule = GeckoSessionTestRule()
 
     @get:Rule val errors = ErrorCollector()
-    fun <T> assertThat(reason: String, v: T, m: Matcher<T>) = sessionRule.assertThat(reason, v, m)
+    fun <T> assertThat(reason: String, v: T, m: Matcher<in T>) = sessionRule.checkThat(reason, v, m)
 
     init {
         if (!noErrorCollector) {
             sessionRule.errorCollector = errors
         }
     }
 
     fun <T> forEachCall(vararg values: T): T = sessionRule.forEachCall(*values)
--- 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];
     }
 }