Bug 1442243 - 4. Add calls for creating sessions; r=snorp draft
authorJim Chen <nchen@mozilla.com>
Wed, 07 Mar 2018 16:12:51 -0500
changeset 764513 ac94362eb3e66ed7e3c8c064be640e77d4ba9895
parent 764512 5d2bc69aa4eb7cc1fb8a5a202cb1114fb643fb16
child 764514 52664740ae6ad218f3bc41e80124ff4e3fe88c03
push id101776
push userbmo:nchen@mozilla.com
push dateWed, 07 Mar 2018 21:13:11 +0000
reviewerssnorp
bugs1442243
milestone60.0a1
Bug 1442243 - 4. Add calls for creating sessions; r=snorp Add createOpenSession and createClosedSession to create new sessions from the test rule. The names are chosen to be explicit about the state of the newly created session. MozReview-Commit-ID: AtLKuyChMwt
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/GeckoSessionTestRuleTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
@@ -683,9 +683,78 @@ class GeckoSessionTestRuleTest {
         assertThat("Wait delegate should be used", waitCounter, equalTo(2))
 
         sessionRule.session.reload()
         sessionRule.waitForPageStop()
 
         assertThat("Test delegate should be used", testCounter, equalTo(6))
         assertThat("Wait delegate should be cleared", waitCounter, equalTo(2))
     }
-}
\ No newline at end of file
+
+    @Test fun wrapSession() {
+        val session = sessionRule.wrapSession(GeckoSession(sessionRule.session.settings))
+        sessionRule.openSession(session)
+        session.reload()
+        session.waitForPageStop()
+    }
+
+    @Test fun createOpenSession() {
+        val newSession = sessionRule.createOpenSession()
+        assertThat("Can create session", newSession, notNullValue())
+        assertThat("New session is open", newSession.isOpen, equalTo(true))
+        assertThat("New session has same settings",
+                   newSession.settings, equalTo(sessionRule.session.settings))
+    }
+
+    @Test fun createOpenSession_withSettings() {
+        val settings = GeckoSessionSettings(sessionRule.session.settings)
+        settings.setBoolean(GeckoSessionSettings.USE_PRIVATE_MODE, true)
+
+        val newSession = sessionRule.createOpenSession(settings)
+        assertThat("New session has same settings", newSession.settings, equalTo(settings))
+    }
+
+    @Test fun createOpenSession_canInterleaveOtherCalls() {
+        sessionRule.session.loadTestPath(HELLO_HTML_PATH)
+
+        val newSession = sessionRule.createOpenSession()
+        sessionRule.session.loadTestPath(HELLO_HTML_PATH)
+        sessionRule.waitForPageStops(2)
+
+        newSession.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
+            @AssertCalled(false)
+            override fun onPageStop(session: GeckoSession, success: Boolean) {
+            }
+        })
+
+        sessionRule.session.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
+            @AssertCalled(count = 2)
+            override fun onPageStop(session: GeckoSession, success: Boolean) {
+            }
+        })
+    }
+
+    @Test fun createClosedSession() {
+        val newSession = sessionRule.createClosedSession()
+        assertThat("Can create session", newSession, notNullValue())
+        assertThat("New session is open", newSession.isOpen, equalTo(false))
+        assertThat("New session has same settings",
+                   newSession.settings, equalTo(sessionRule.session.settings))
+    }
+
+    @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)
+    @TimeoutMillis(1000)
+    @LargeTest
+    @GeckoSessionTestRule.ClosedSessionAtStart
+    fun noPendingCallbacks_withSpecificSession() {
+        sessionRule.createOpenSession()
+        // Make sure we don't have unexpected pending callbacks after opening a session.
+        sessionRule.waitUntilCalled(object : Callbacks.All {})
+    }
+}
--- 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
@@ -47,16 +47,17 @@ import java.lang.reflect.ParameterizedTy
 import java.lang.reflect.Proxy;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.regex.Pattern;
 
 import kotlin.jvm.JvmClassMappingKt;
 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
@@ -64,16 +65,30 @@ import kotlin.reflect.KClass;
  * callbacks are called in the proper order.
  */
 public class GeckoSessionTestRule extends UiThreadTestRule {
 
     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;
+
+    static {
+        try {
+            sOnLocationChange = GeckoSession.NavigationDelegate.class.getMethod(
+                    "onLocationChange", GeckoSession.class, String.class);
+            sOnPageStop = GeckoSession.ProgressDelegate.class.getMethod(
+                    "onPageStop", GeckoSession.class, boolean.class);
+        } catch (final NoSuchMethodException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     /**
      * Specify the timeout for any of the wait methods, in milliseconds. Can be used
      * on classes or methods.
      */
     @Target({ElementType.METHOD, ElementType.TYPE})
     @Retention(RetentionPolicy.RUNTIME)
     public @interface TimeoutMillis {
         long value();
@@ -347,16 +362,20 @@ public class GeckoSessionTestRule extend
 
         public CallRecord(final Method method, final Object[] args) {
             this.method = method;
             this.methodCall = new MethodCall(method, /* requirement */ null);
             this.args = args;
         }
     }
 
+    protected interface CallRecordHandler {
+        boolean handleCall(Method method, Object[] args);
+    }
+
     public class Environment {
         /* package */ Environment() {
         }
 
         private String getEnvVar(final String name) {
             final int nameLen = name.length();
             final Bundle args = InstrumentationRegistry.getArguments();
             String env = args.getString("env0", null);
@@ -371,17 +390,17 @@ public class GeckoSessionTestRule extend
             return "";
         }
 
         public boolean isAutomation() {
             return !getEnvVar("MOZ_IN_AUTOMATION").isEmpty();
         }
 
         public boolean isE10s() {
-            return mSession.getSettings().getBoolean(
+            return mMainSession.getSettings().getBoolean(
                     GeckoSessionSettings.USE_MULTIPROCESS);
         }
 
         public boolean isDebugging() {
             return Debug.isDebuggerConnected();
         }
     }
 
@@ -476,28 +495,30 @@ public class GeckoSessionTestRule extend
 
     private static final List<Class<?>> CALLBACK_CLASSES = Arrays.asList(getCallbackClasses());
 
     public final Environment env = new Environment();
 
     protected final Instrumentation mInstrumentation =
             InstrumentationRegistry.getInstrumentation();
     protected final GeckoSessionSettings mDefaultSettings;
+    protected final Set<GeckoSession> mSubSessions = new HashSet<>();
 
     protected ErrorCollector mErrorCollector;
-    protected GeckoSession mSession;
-    protected Point mDisplaySize;
+    protected GeckoSession mMainSession;
     protected Object mCallbackProxy;
     protected List<CallRecord> mCallRecords;
+    protected CallRecordHandler mCallRecordHandler;
     protected CallbackDelegates mWaitScopeDelegates;
     protected CallbackDelegates mTestScopeDelegates;
     protected int mLastWaitStart;
     protected int mLastWaitEnd;
     protected MethodCall mCurrentMethodCall;
     protected long mTimeoutMillis;
+    protected Point mDisplaySize;
     protected SurfaceTexture mDisplayTexture;
     protected Surface mDisplaySurface;
     protected GeckoDisplay mDisplay;
     protected boolean mClosedSession;
 
     public GeckoSessionTestRule() {
         mDefaultSettings = new GeckoSessionSettings();
     }
@@ -563,17 +584,17 @@ public class GeckoSessionTestRule extend
     }
 
     /**
      * Get the session set up for the current test.
      *
      * @return GeckoSession object.
      */
     public @NonNull GeckoSession getSession() {
-        return mSession;
+        return mMainSession;
     }
 
     protected static Method getCallbackSetter(final @NonNull Class<?> cls)
             throws NoSuchMethodException {
         return GeckoSession.class.getMethod("set" + cls.getSimpleName(), cls);
     }
 
     protected static Method getCallbackGetter(final @NonNull Class<?> cls)
@@ -596,26 +617,28 @@ public class GeckoSessionTestRule extend
                 final WithDisplay displaySize = (WithDisplay)annotation;
                 mDisplaySize = new Point(displaySize.width(), displaySize.height());
             } else if (ClosedSessionAtStart.class.equals(annotation.annotationType())) {
                 mClosedSession = ((ClosedSessionAtStart) annotation).value();
             }
         }
     }
 
-    private static RuntimeException unwrapRuntimeException(Throwable e) {
+    private static RuntimeException unwrapRuntimeException(final Throwable e) {
         final Throwable cause = e.getCause();
         if (cause != null && cause instanceof RuntimeException) {
-            return (RuntimeException)cause;
+            return (RuntimeException) cause;
+        } else if (e instanceof RuntimeException) {
+            return (RuntimeException) e;
         }
 
-        return new RuntimeException(cause);
+        return new RuntimeException(cause != null ? cause : e);
     }
 
-    protected void prepareSession(final Description description) throws Throwable {
+    protected void prepareStatement(final Description description) throws Throwable {
         final GeckoSessionSettings settings = new GeckoSessionSettings(mDefaultSettings);
         mTimeoutMillis = !env.isDebugging() ? DEFAULT_TIMEOUT_MILLIS
                                             : DEFAULT_DEBUG_TIMEOUT_MILLIS;
         mClosedSession = false;
 
         applyAnnotations(Arrays.asList(description.getTestClass().getAnnotations()), settings);
         applyAnnotations(description.getAnnotations(), settings);
 
@@ -627,24 +650,37 @@ public class GeckoSessionTestRule extend
         mTestScopeDelegates = testDelegates;
         mLastWaitStart = 0;
         mLastWaitEnd = 0;
 
         final InvocationHandler recorder = new InvocationHandler() {
             @Override
             public Object invoke(final Object proxy, final Method method,
                                  final Object[] args) {
-                assertThat("Callbacks must be on UI thread",
-                           Looper.myLooper(), equalTo(Looper.getMainLooper()));
+                boolean ignore = false;
+                MethodCall call = null;
+
+                if (Object.class.equals(method.getDeclaringClass())) {
+                    ignore = true;
+                } else if (mCallRecordHandler != null) {
+                    ignore = mCallRecordHandler.handleCall(method, args);
+                }
 
-                records.add(new CallRecord(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[0], instanceOf(GeckoSession.class));
 
-                MethodCall call = waitDelegates.prepareMethodCall(method);
-                if (call == null) {
-                    call = testDelegates.prepareMethodCall(method);
+                    records.add(new CallRecord(method, args));
+
+                    call = waitDelegates.prepareMethodCall(method);
+                    if (call == null) {
+                        call = testDelegates.prepareMethodCall(method);
+                    }
                 }
 
                 try {
                     mCurrentMethodCall = call;
                     return method.invoke((call != null) ? call.target
                                                         : Callbacks.Default.INSTANCE, args);
                 } catch (final IllegalAccessException | InvocationTargetException e) {
                     throw unwrapRuntimeException(e);
@@ -653,87 +689,145 @@ public class GeckoSessionTestRule extend
                 }
             }
         };
 
         final Class<?>[] classes = CALLBACK_CLASSES.toArray(new Class<?>[CALLBACK_CLASSES.size()]);
         mCallbackProxy = Proxy.newProxyInstance(GeckoSession.class.getClassLoader(),
                                                 classes, recorder);
 
-        mSession = new GeckoSession(settings);
+        mMainSession = new GeckoSession(settings);
+        prepareSession(mMainSession);
 
         if (mDisplaySize != null) {
             mDisplayTexture = new SurfaceTexture(0);
             mDisplaySurface = new Surface(mDisplayTexture);
-            mDisplay = mSession.acquireDisplay();
+            mDisplay = mMainSession.acquireDisplay();
             mDisplay.surfaceChanged(mDisplaySurface, mDisplaySize.x, mDisplaySize.y);
         }
 
+        if (!mClosedSession) {
+            openSession(mMainSession);
+        }
+    }
+
+    protected void prepareSession(final GeckoSession session) throws Throwable {
         for (final Class<?> cls : CALLBACK_CLASSES) {
             if (cls != null) {
-                getCallbackSetter(cls).invoke(mSession, mCallbackProxy);
+                getCallbackSetter(cls).invoke(session, mCallbackProxy);
             }
         }
+    }
 
-        if (mClosedSession) {
+    /**
+     * Call openWindow() 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.openWindow(mInstrumentation.getTargetContext());
+
+        if (!e10s) {
             return;
         }
 
-        mSession.openWindow(mInstrumentation.getTargetContext());
+        // Under e10s, 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;
 
-        if (settings.getBoolean(GeckoSessionSettings.USE_MULTIPROCESS)) {
-            // Under e10s, we receive an initial about:blank load; don't expose that to the test.
-            waitForPageStop();
+                @Override
+                public boolean handleCall(final Method method, final Object[] args) {
+                    if (!mFoundStart && sOnLocationChange.equals(method) &&
+                            session.equals(args[0]) && "about:blank".equals(args[1])) {
+                        mFoundStart = true;
+                        return true;
+                    } else if (mFoundStart && session.equals(args[0])) {
+                        if (sOnPageStop.equals(method)) {
+                            mCallRecordHandler = null;
+                        }
+                        return true;
+                    }
+                    return false;
+                }
+            };
+
+            do {
+                loopUntilIdle(mTimeoutMillis);
+            } while (mCallRecordHandler != null);
+
+        } finally {
+            mCallRecordHandler = null;
         }
     }
 
     /**
      * Internal method to perform callback checks at the end of a test.
      */
     public void performTestEndCheck() {
         mWaitScopeDelegates.clear();
         mTestScopeDelegates.clear();
     }
 
-    protected void cleanupSession() throws Throwable {
-        if (mSession.isOpen()) {
-            mSession.closeWindow();
+    protected void cleanupSession(final GeckoSession session) {
+        if (session.isOpen()) {
+            session.closeWindow();
         }
+    }
+
+    protected void cleanupStatement() throws Throwable {
+        for (final GeckoSession session : mSubSessions) {
+            cleanupSession(session);
+        }
+        cleanupSession(mMainSession);
 
         if (mDisplay != null) {
             mDisplay.surfaceDestroyed();
-            mSession.releaseDisplay(mDisplay);
+            mMainSession.releaseDisplay(mDisplay);
+            mDisplay = null;
             mDisplaySurface.release();
+            mDisplaySurface = null;
             mDisplayTexture.release();
-            mDisplay = null;
             mDisplayTexture = null;
-            mDisplaySurface = null;
         }
 
-        mSession = null;
+        mMainSession = null;
         mCallbackProxy = null;
         mCallRecords = null;
         mWaitScopeDelegates = null;
         mTestScopeDelegates = null;
         mLastWaitStart = 0;
         mLastWaitEnd = 0;
         mTimeoutMillis = 0;
     }
 
     @Override
     public Statement apply(final Statement base, final Description description) {
         return super.apply(new Statement() {
             @Override
             public void evaluate() throws Throwable {
                 try {
-                    prepareSession(description);
+                    prepareStatement(description);
                     base.evaluate();
                     performTestEndCheck();
                 } finally {
-                    cleanupSession();
+                    cleanupStatement();
                 }
             }
         }, description);
     }
 
     @Override
     protected boolean shouldRunOnUiThread(final Description description) {
         return true;
@@ -821,26 +915,18 @@ public class GeckoSessionTestRule extend
 
     /**
      * 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 count Number of page loads to wait for.
      */
     public void waitForPageStops(final int count) {
-        final Method onPageStop;
-        try {
-            onPageStop = GeckoSession.ProgressDelegate.class.getMethod(
-                    "onPageStop", GeckoSession.class, boolean.class);
-        } catch (final NoSuchMethodException e) {
-            throw new RuntimeException(e);
-        }
-
         final List<MethodCall> methodCalls = new ArrayList<>(1);
-        methodCalls.add(new MethodCall(onPageStop,
+        methodCalls.add(new MethodCall(sOnPageStop,
                 new CallRequirement(/* allowed */ true, count, null)));
 
         waitUntilCalled(GeckoSession.ProgressDelegate.class, methodCalls);
     }
 
     /**
      * 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.
@@ -1078,20 +1164,90 @@ public class GeckoSessionTestRule extend
      * {@link #delegateUntilTestEnd}.
      *
      * @param callback Callback object, or null to clear all previously-set delegates.
      */
     public void delegateDuringNextWait(final Object callback) {
         mWaitScopeDelegates.delegate(callback);
     }
 
-    public void synthesizeTap(int x, int y) {
+    /**
+     * Synthesize a tap event at the specified location using the main session.
+     * The session must have been created with a display.
+     *
+     * @param x X coordinate
+     * @param y Y coordinate
+     */
+    public void synthesizeTap(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);
-
+        final MotionEvent down = MotionEvent.obtain(
+                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, x, y, 0);
         mSession.getPanZoomController().onTouchEvent(down);
 
-        final MotionEvent up = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
-                MotionEvent.ACTION_UP, x, y, 0);
+        final MotionEvent up = MotionEvent.obtain(
+                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0);
         mSession.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
+     */
+    public GeckoSession wrapSession(final GeckoSession session) {
+        try {
+            mSubSessions.add(session);
+            prepareSession(session);
+        } catch (final Throwable e) {
+            throw unwrapRuntimeException(e);
+        }
+        return session;
+    }
+
+    private GeckoSession createSession(final GeckoSessionSettings settings,
+                                       final boolean open) {
+        final GeckoSession session = wrapSession(new GeckoSession(settings));
+        if (open) {
+            openSession(session);
+        }
+        return session;
+    }
+
+    /**
+     * Create a new, opened session using the main session settings.
+     *
+     * @return New session.
+     */
+    public GeckoSession createOpenSession() {
+        return createSession(mMainSession.getSettings(), /* open */ true);
+    }
+
+    /**
+     * Create a new, opened session using the specified settings.
+     *
+     * @param settings Settings for the new session.
+     * @return New session.
+     */
+    public GeckoSession createOpenSession(final GeckoSessionSettings settings) {
+        return createSession(settings, /* open */ true);
+    }
+
+    /**
+     * Create a new, closed session using the specified settings.
+     *
+     * @return New session.
+     */
+    public GeckoSession createClosedSession() {
+        return createSession(mMainSession.getSettings(), /* open */ false);
+    }
+
+    /**
+     * Create a new, closed session using the specified settings.
+     *
+     * @param settings Settings for the new session.
+     * @return New session.
+     */
+    public GeckoSession createClosedSession(final GeckoSessionSettings settings) {
+        return createSession(settings, /* open */ false);
+    }
 }