Bug 1442243 - 5. Add session-specific versions of test rule calls; r?snorp draft
authorJim Chen <nchen@mozilla.com>
Wed, 07 Mar 2018 16:12:51 -0500
changeset 764514 52664740ae6ad218f3bc41e80124ff4e3fe88c03
parent 764513 ac94362eb3e66ed7e3c8c064be640e77d4ba9895
child 764515 6d586babf5da3eaab4fd52f2fd56908d15edc801
push id101776
push userbmo:nchen@mozilla.com
push dateWed, 07 Mar 2018 21:13:11 +0000
reviewerssnorp
bugs1442243
milestone60.0a1
Bug 1442243 - 5. Add session-specific versions of test rule calls; r?snorp Keep track of the session of recorded calls, and add session-specific overrides of the wait*, for*, and delegate* calls that allow operations on a particular session. The session-less call will operate on all sessions. MozReview-Commit-ID: 170FPHw3DwR
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
--- 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
@@ -26,16 +26,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.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;
 import java.lang.annotation.Target;
@@ -261,50 +262,55 @@ public class GeckoSessionTestRule extend
 
         /* package */ CallInfo(final int counter, final int order) {
             this.counter = counter;
             this.order = order;
         }
     }
 
     public static class MethodCall {
+        public final GeckoSession session;
         public final Method method;
         public final CallRequirement requirement;
         public final Object target;
         private int currentCount;
 
-        public MethodCall(final Method method,
+        public MethodCall(final GeckoSession session, final Method method,
                           final CallRequirement requirement) {
-            this(method, requirement, /* target */ null);
+            this(session, method, requirement, /* target */ null);
         }
 
-        /* package */ MethodCall(final Method method, final AssertCalled annotation,
-                                 final Object target) {
-            this(method,
+        /* package */ MethodCall(final GeckoSession session, final Method method,
+                                 final AssertCalled annotation, final Object target) {
+            this(session, method,
                  (annotation != null) ? new CallRequirement(annotation.value(),
                                                             annotation.count(),
                                                             annotation.order())
                                       : null,
                  /* target */ target);
         }
 
-        /* package */ MethodCall(final Method method, final CallRequirement requirement,
-                                 final Object target) {
+        /* package */ MethodCall(final GeckoSession session, final Method method,
+                                 final CallRequirement requirement, final Object target) {
+            this.session = session;
             this.method = method;
             this.requirement = requirement;
             this.target = target;
             currentCount = 0;
         }
 
         @Override
         public boolean equals(final Object other) {
             if (this == other) {
                 return true;
             } else if (other instanceof MethodCall) {
-                return methodsEqual(method, ((MethodCall) other).method);
+                final MethodCall otherCall = (MethodCall) other;
+                return (session == null || otherCall.session == null ||
+                        session == otherCall.session) &&
+                        methodsEqual(method, ((MethodCall) other).method);
             } else if (other instanceof Method) {
                 return methodsEqual(method, (Method) other);
             }
             return false;
         }
 
         /* package */ int getOrder() {
             if (requirement == null || currentCount == 0) {
@@ -355,19 +361,19 @@ public class GeckoSessionTestRule extend
         }
     }
 
     protected static class CallRecord {
         public final Method method;
         public final MethodCall methodCall;
         public final Object[] args;
 
-        public CallRecord(final Method method, final Object[] args) {
+        public CallRecord(final GeckoSession session, final Method method, final Object[] args) {
             this.method = method;
-            this.methodCall = new MethodCall(method, /* requirement */ null);
+            this.methodCall = new MethodCall(session, method, /* requirement */ null);
             this.args = args;
         }
     }
 
     protected interface CallRecordHandler {
         boolean handleCall(Method method, Object[] args);
     }
 
@@ -400,53 +406,58 @@ public class GeckoSessionTestRule extend
         }
 
         public boolean isDebugging() {
             return Debug.isDebuggerConnected();
         }
     }
 
     protected class CallbackDelegates {
-        private final Map<Method, MethodCall> mDelegates = new HashMap<>();
+        private final Map<Pair<GeckoSession, Method>, MethodCall> mDelegates = new HashMap<>();
         private int mOrder;
 
-        public void delegate(final Object callback) {
+        public void delegate(final @Nullable GeckoSession session,
+                             final @NonNull Object callback) {
             for (final Class<?> ifce : CALLBACK_CLASSES) {
                 if (!ifce.isInstance(callback)) {
                     continue;
                 }
                 for (final Method method : ifce.getMethods()) {
                     final Method callbackMethod;
                     try {
                         callbackMethod = callback.getClass().getMethod(method.getName(),
                                                                        method.getParameterTypes());
                     } catch (final NoSuchMethodException e) {
                         throw new RuntimeException(e);
                     }
                     final MethodCall call = new MethodCall(
-                            callbackMethod, getAssertCalled(callbackMethod, callback), callback);
-                    mDelegates.put(method, call);
+                            session, callbackMethod,
+                            getAssertCalled(callbackMethod, callback), callback);
+                    mDelegates.put(new Pair<>(session, method), call);
                 }
             }
         }
 
         public void clear() {
             final Collection<MethodCall> values = mDelegates.values();
             final MethodCall[] valuesArray = values.toArray(new MethodCall[values.size()]);
 
             mDelegates.clear();
             mOrder = 0;
 
             for (final MethodCall call : valuesArray) {
                 assertMatchesCount(call);
             }
         }
 
-        public MethodCall prepareMethodCall(final Method method) {
-            final MethodCall call = mDelegates.get(method);
+        public MethodCall prepareMethodCall(final GeckoSession session, final Method method) {
+            MethodCall call = mDelegates.get(new Pair<>(session, method));
+            if (call == null && session != null) {
+                call = mDelegates.get(new Pair<>((GeckoSession) null, method));
+            }
             if (call == null) {
                 return null;
             }
 
             assertAllowMoreCalls(call);
             call.incrementCounter();
             assertOrder(call, mOrder);
             mOrder = Math.max(call.getOrder(), mOrder);
@@ -665,21 +676,22 @@ public class GeckoSessionTestRule extend
                 }
 
                 if (!ignore) {
                     assertThat("Callbacks must be on UI thread",
                                Looper.myLooper(), equalTo(Looper.getMainLooper()));
                     assertThat("Callback first argument must be session object",
                                args[0], instanceOf(GeckoSession.class));
 
-                    records.add(new CallRecord(method, args));
+                    final GeckoSession session = (GeckoSession) args[0];
+                    records.add(new CallRecord(session, method, args));
 
-                    call = waitDelegates.prepareMethodCall(method);
+                    call = waitDelegates.prepareMethodCall(session, method);
                     if (call == null) {
-                        call = testDelegates.prepareMethodCall(method);
+                        call = testDelegates.prepareMethodCall(session, method);
                     }
                 }
 
                 try {
                     mCurrentMethodCall = call;
                     return method.invoke((call != null) ? call.target
                                                         : Callbacks.Default.INSTANCE, args);
                 } catch (final IllegalAccessException | InvocationTargetException e) {
@@ -901,57 +913,108 @@ public class GeckoSessionTestRule extend
         } finally {
             if (timeout > 0) {
                 handler.removeCallbacks(timeoutRunnable);
             }
         }
     }
 
     /**
+     * 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);
+    }
+
+    /**
      * Wait until a page load has finished. The session must have started a page load since
      * the last wait, or this method will wait indefinitely.
+     *
+     * @param session Session to wait on, or null to wait on any session.
      */
-    public void waitForPageStop() {
-        waitForPageStops(/* count */ 1);
+    public void waitForPageStop(final GeckoSession session) {
+        waitForPageStops(session, /* count */ 1);
+    }
+
+    /**
+     * 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.
+     *
+     * @param count Number of page loads to wait for.
+     */
+    public void waitForPageStops(final int count) {
+        waitForPageStops(/* session */ null, count);
     }
 
     /**
      * Wait until a page load has finished. The session must have started a page load since
      * the last wait, or this method will wait indefinitely.
      *
+     * @param session Session to wait on, or null to wait on any session.
      * @param count Number of page loads to wait for.
      */
-    public void waitForPageStops(final int count) {
+    public void waitForPageStops(final GeckoSession session, final int count) {
         final List<MethodCall> methodCalls = new ArrayList<>(1);
-        methodCalls.add(new MethodCall(sOnPageStop,
+        methodCalls.add(new MethodCall(session, sOnPageStop,
                 new CallRequirement(/* allowed */ true, count, null)));
 
-        waitUntilCalled(GeckoSession.ProgressDelegate.class, methodCalls);
+        waitUntilCalled(session, GeckoSession.ProgressDelegate.class, methodCalls);
+    }
+
+    /**
+     * Wait until the specified methods have been called on the specified callback
+     * interface for any session. If no methods are specified, wait until any method has
+     * been called.
+     *
+     * @param callback Target callback interface; must be an interface under GeckoSession.
+     * @param methods List of methods to wait on; use empty or null or wait on any method.
+     */
+    public void waitUntilCalled(final @NonNull KClass<?> callback,
+                                final @Nullable String... methods) {
+        waitUntilCalled(/* session */ null, callback, methods);
     }
 
     /**
      * Wait until the specified methods have been called on the specified callback
      * interface. If no methods are specified, wait until any method has been called.
      *
+     * @param session Session to wait on, or null to wait on any session.
      * @param callback Target callback interface; must be an interface under GeckoSession.
      * @param methods List of methods to wait on; use empty or null or wait on any method.
      */
-    public void waitUntilCalled(final @NonNull KClass<?> callback,
+    public void waitUntilCalled(final @Nullable GeckoSession session,
+                                final @NonNull KClass<?> callback,
                                 final @Nullable String... methods) {
-        waitUntilCalled(JvmClassMappingKt.getJavaClass(callback), methods);
+        waitUntilCalled(session, JvmClassMappingKt.getJavaClass(callback), methods);
+    }
+
+    /**
+     * Wait until the specified methods have been called on the specified callback
+     * interface for any session. If no methods are specified, wait until any method has
+     * been called.
+     *
+     * @param callback Target callback interface; must be an interface under GeckoSession.
+     * @param methods List of methods to wait on; use empty or null or wait on any method.
+     */
+    public void waitUntilCalled(final @NonNull Class<?> callback,
+                                final @Nullable String... methods) {
+        waitUntilCalled(/* session */ null, callback, methods);
     }
 
     /**
      * Wait until the specified methods have been called on the specified callback
      * interface. If no methods are specified, wait until any method has been called.
      *
+     * @param session Session to wait on, or null to wait on any session.
      * @param callback Target callback interface; must be an interface under GeckoSession.
      * @param methods List of methods to wait on; use empty or null or wait on any method.
      */
-    public void waitUntilCalled(final @NonNull Class<?> callback,
+    public void waitUntilCalled(final @Nullable GeckoSession session,
+                                final @NonNull Class<?> callback,
                                 final @Nullable String... methods) {
         final int length = (methods != null) ? methods.length : 0;
         final Pattern[] patterns = new Pattern[length];
         for (int i = 0; i < length; i++) {
             patterns[i] = Pattern.compile(methods[i]);
         }
 
         final List<MethodCall> waitMethods = new ArrayList<>();
@@ -961,40 +1024,55 @@ public class GeckoSessionTestRule extend
             if (!ifce.isAssignableFrom(callback)) {
                 continue;
             }
             for (final Method method : ifce.getMethods()) {
                 for (final Pattern pattern : patterns) {
                     if (!pattern.matcher(method.getName()).matches()) {
                         continue;
                     }
-                    waitMethods.add(new MethodCall(method, /* requirement */ null));
+                    waitMethods.add(new MethodCall(session, method,
+                                                   /* requirement */ null));
                     break;
                 }
             }
             isSessionCallback = true;
         }
 
         assertThat("Class should be a GeckoSession interface",
                    isSessionCallback, equalTo(true));
 
-        waitUntilCalled(callback, waitMethods);
+        waitUntilCalled(session, callback, waitMethods);
+    }
+
+    /**
+     * Wait until the specified methods have been called on the specified object for any
+     * session, as specified by any {@link AssertCalled @AssertCalled} annotations. If no
+     * {@link AssertCalled @AssertCalled} annotations are found, wait until any method has
+     * been called. Only methods belonging to a GeckoSession callback are supported.
+     *
+     * @param callback Target callback object; must implement an interface under GeckoSession.
+     */
+    public void waitUntilCalled(final @NonNull Object callback) {
+        waitUntilCalled(/* session */ null, callback);
     }
 
     /**
      * Wait until the specified methods have been called on the specified object,
      * as specified by any {@link AssertCalled @AssertCalled} annotations. If no
      * {@link AssertCalled @AssertCalled} annotations are found, wait until any method
      * has been called. Only methods belonging to a GeckoSession callback are supported.
      *
+     * @param session Session to wait on, or null to wait on any session.
      * @param callback Target callback object; must implement an interface under GeckoSession.
      */
-    public void waitUntilCalled(final @NonNull Object callback) {
+    public void waitUntilCalled(final @Nullable GeckoSession session,
+                                final @NonNull Object callback) {
         if (callback instanceof Class<?>) {
-            waitUntilCalled((Class<?>) callback, (String[]) null);
+            waitUntilCalled(session, (Class<?>) callback, (String[]) null);
             return;
         }
 
         final List<MethodCall> methodCalls = new ArrayList<>();
         for (final Class<?> ifce : CALLBACK_CLASSES) {
             if (!ifce.isInstance(callback)) {
                 continue;
             }
@@ -1003,35 +1081,44 @@ public class GeckoSessionTestRule extend
                 try {
                     callbackMethod = callback.getClass().getMethod(method.getName(),
                                                                    method.getParameterTypes());
                 } catch (final NoSuchMethodException e) {
                     throw new RuntimeException(e);
                 }
                 final AssertCalled ac = getAssertCalled(callbackMethod, callback);
                 if (ac != null && ac.value()) {
-                    methodCalls.add(new MethodCall(callbackMethod, ac, /* target */ null));
+                    methodCalls.add(new MethodCall(session, callbackMethod,
+                                                   ac, /* target */ null));
                 }
             }
         }
 
-        waitUntilCalled(callback.getClass(), methodCalls);
-        forCallbacksDuringWait(callback);
+        waitUntilCalled(session, callback.getClass(), methodCalls);
+        forCallbacksDuringWait(session, callback);
     }
 
-    protected void waitUntilCalled(final @NonNull Class<?> delegate,
+    protected void waitUntilCalled(final @Nullable GeckoSession session,
+                                   final @NonNull Class<?> delegate,
                                    final @NonNull List<MethodCall> methodCalls) {
+        if (session != null && !session.equals(mMainSession)) {
+            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) {
             try {
                 assertThat("Callbacks should be set through" +
                            " GeckoSessionTestRule delegate methods",
-                           getCallbackGetter(ifce).invoke(mSession), sameInstance(mCallbackProxy));
+                           getCallbackGetter(ifce).invoke(session == null ? mMainSession
+                                                                          : session),
+                           sameInstance(mCallbackProxy));
             } catch (final NoSuchMethodException | IllegalAccessException |
                            InvocationTargetException e) {
                 throw unwrapRuntimeException(e);
             }
         }
 
         boolean calledAny = false;
         int index = mLastWaitStart = mLastWaitEnd;
@@ -1057,52 +1144,69 @@ public class GeckoSessionTestRule extend
             }
         }
 
         mLastWaitEnd = index;
         mWaitScopeDelegates.clear();
     }
 
     /**
+     * Playback callbacks that were made on all sessions during the previous wait. For any
+     * methods annotated with {@link AssertCalled @AssertCalled}, assert that the
+     * callbacks satisfy the specified requirements. If no {@link AssertCalled
+     * @AssertCalled} annotations are found, assert any method has been called. Only
+     * methods belonging to a GeckoSession callback are supported.
+     *
+     * @param callback Target callback object; must implement one or more interfaces
+     *                 under GeckoSession.
+     */
+    public void forCallbacksDuringWait(final @NonNull Object callback) {
+        forCallbacksDuringWait(/* session */ null, callback);
+    }
+
+    /**
      * Playback callbacks that were made during the previous wait. For any methods
      * annotated with {@link AssertCalled @AssertCalled}, assert that the callbacks
      * satisfy the specified requirements. If no {@link AssertCalled @AssertCalled}
      * annotations are found, assert any method has been called. Only methods belonging
      * to a GeckoSession callback are supported.
      *
+     * @param session  Target session object, or null to playback all sessions.
      * @param callback Target callback object; must implement one or more interfaces
      *                 under GeckoSession.
      */
-    public void forCallbacksDuringWait(final @NonNull Object callback) {
+    public void forCallbacksDuringWait(final @Nullable GeckoSession session,
+                                       final @NonNull Object callback) {
         final Method[] declaredMethods = callback.getClass().getDeclaredMethods();
         final List<MethodCall> methodCalls = new ArrayList<>(declaredMethods.length);
         for (final Class<?> ifce : CALLBACK_CLASSES) {
             if (!ifce.isInstance(callback)) {
                 continue;
             }
             for (final Method method : ifce.getMethods()) {
                 final Method callbackMethod;
                 try {
                     callbackMethod = callback.getClass().getMethod(method.getName(),
                                                                    method.getParameterTypes());
                 } catch (final NoSuchMethodException e) {
                     throw new RuntimeException(e);
                 }
                 methodCalls.add(new MethodCall(
-                        callbackMethod, getAssertCalled(callbackMethod, callback),
+                        session, callbackMethod, getAssertCalled(callbackMethod, callback),
                         /* target */ null));
             }
         }
 
         int order = 0;
         boolean calledAny = false;
 
         for (int index = mLastWaitStart; index < mLastWaitEnd; index++) {
             final CallRecord record = mCallRecords.get(index);
-            if (!record.method.getDeclaringClass().isInstance(callback)) {
+            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));
 
             final MethodCall methodCall = methodCalls.get(i);
@@ -1141,55 +1245,85 @@ public class GeckoSessionTestRule extend
      * @return Call information
      */
     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 {@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);
+    }
+
+    /**
      * Delegate implemented interfaces to the specified callback object, for the rest of the test.
      * Only GeckoSession callback interfaces are supported. Delegates for {@link
      * #delegateUntilTestEnd} can be temporarily overridden by delegates for {@link
      * #delegateDuringNextWait}.
      *
+     * @param session Session to target, or null to target all sessions.
      * @param callback Callback object, or null to clear all previously-set delegates.
      */
-    public void delegateUntilTestEnd(final Object callback) {
-        mTestScopeDelegates.delegate(callback);
+    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 {@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);
     }
 
     /**
      * Delegate implemented interfaces to the specified callback object, during the next wait.
      * Only GeckoSession callback interfaces are supported. Delegates for {@link
      * #delegateDuringNextWait} can temporarily take precedence over delegates for
      * {@link #delegateUntilTestEnd}.
      *
+     * @param session Session to target, or null to target all sessions.
      * @param callback Callback object, or null to clear all previously-set delegates.
      */
-    public void delegateDuringNextWait(final Object callback) {
-        mWaitScopeDelegates.delegate(callback);
+    public void delegateDuringNextWait(final @Nullable GeckoSession session,
+                                       final @NonNull Object callback) {
+        mWaitScopeDelegates.delegate(session, callback);
     }
 
     /**
      * Synthesize a tap event at the specified location using the main session.
      * The session must have been created with a display.
      *
+     * @param session Target session
      * @param x X coordinate
      * @param y Y coordinate
      */
-    public void synthesizeTap(final int x, final int y) {
+    public void synthesizeTap(final @NonNull GeckoSession session,
+                              final int x, final int y) {
         final long downTime = SystemClock.uptimeMillis();
         final MotionEvent down = MotionEvent.obtain(
                 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, x, y, 0);
-        mSession.getPanZoomController().onTouchEvent(down);
+        session.getPanZoomController().onTouchEvent(down);
 
         final MotionEvent up = MotionEvent.obtain(
                 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0);
-        mSession.getPanZoomController().onTouchEvent(up);
+        session.getPanZoomController().onTouchEvent(up);
     }
 
     /**
      * Initialize and keep track of the specified session within the test rule. The
      * session is automatically cleaned up at the end of the test.
      *
      * @param session Session to keep track of.
      * @return Same session