Bug 1456543 - 1. Add @NullDelegate test annotation; r?snorp draft
authorJim Chen <nchen@mozilla.com>
Fri, 27 Apr 2018 11:57:13 -0400
changeset 789100 9b1bbb927ad88d5efe3808f5d3c7c86d0fc212c4
parent 788439 480cf7fe96670d904b0c99d72b4d0e5beb1221cf
child 789101 b963cc20cde4017b9b27d658be6d095e05997d2b
push id108179
push userbmo:nchen@mozilla.com
push dateFri, 27 Apr 2018 15:58:18 +0000
reviewerssnorp
bugs1456543
milestone61.0a1
Bug 1456543 - 1. Add @NullDelegate test annotation; r?snorp Add an annotation for setting a particular delegate to null on test start instead of to the proxy object. Assert that a null-delegate is never used for any of the wait or delegate methods, because those methods would never succeed with a null-delegate. MozReview-Commit-ID: DhvOIJXoMCh
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
@@ -10,16 +10,17 @@ import org.mozilla.geckoview.BuildConfig
 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.assertThat;
+import static org.junit.Assert.fail;
 
 import org.hamcrest.Matcher;
 
 import org.junit.rules.ErrorCollector;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
 import android.app.Instrumentation;
@@ -125,16 +126,33 @@ public class GeckoSessionTestRule extend
      */
     @Target({ElementType.METHOD, ElementType.TYPE})
     @Retention(RetentionPolicy.RUNTIME)
     public @interface ClosedSessionAtStart {
         boolean value() default true;
     }
 
     /**
+     * Specify that the test will set a delegate to null when creating a session, rather
+     * than setting the delegate to a proxy. The test cannot wait on any delegates that
+     * are set to null.
+     */
+    @Target({ElementType.METHOD, ElementType.TYPE})
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface NullDelegate {
+        Class<?> value();
+
+        @Target({ElementType.METHOD, ElementType.TYPE})
+        @Retention(RetentionPolicy.RUNTIME)
+        @interface List {
+            NullDelegate[] value();
+        }
+    }
+
+    /**
      * Specify a list of GeckoSession settings to be applied to the GeckoSession object
      * under test. Can be used on classes or methods. Note that the settings values must
      * be string literals regardless of the type of the settings.
      * <p>
      * Disable e10s for a particular test:
      * <pre>
      * &#64;Setting.List(&#64;Setting(key = Setting.Key.USE_MULTIPROCESS,
      *                        value = "false"))
@@ -448,16 +466,19 @@ public class GeckoSessionTestRule extend
         private int mOrder;
 
         public void delegate(final @Nullable GeckoSession session,
                              final @NonNull Object callback) {
             for (final Class<?> ifce : CALLBACK_CLASSES) {
                 if (!ifce.isInstance(callback)) {
                     continue;
                 }
+                assertThat("Cannot delegate null-delegate callbacks",
+                           ifce, not(isIn(mNullDelegates)));
+
                 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);
                     }
@@ -546,16 +567,17 @@ public class GeckoSessionTestRule extend
     protected final Instrumentation mInstrumentation =
             InstrumentationRegistry.getInstrumentation();
     protected final GeckoSessionSettings mDefaultSettings;
     protected final Set<GeckoSession> mSubSessions = new HashSet<>();
 
     protected ErrorCollector mErrorCollector;
     protected GeckoSession mMainSession;
     protected Object mCallbackProxy;
+    protected Set<Class<?>> mNullDelegates;
     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;
@@ -659,30 +681,48 @@ public class GeckoSessionTestRule extend
         return GeckoSession.class.getMethod("set" + cls.getSimpleName(), cls);
     }
 
     protected static Method getCallbackGetter(final @NonNull Class<?> cls)
             throws NoSuchMethodException {
         return GeckoSession.class.getMethod("get" + cls.getSimpleName());
     }
 
+    private void addNullDelegate(final Class<?> delegate) {
+        if (!Callbacks.class.equals(delegate.getDeclaringClass())) {
+            assertThat("Null-delegate must be valid interface class",
+                       delegate, isIn(CALLBACK_CLASSES));
+            mNullDelegates.add(delegate);
+            return;
+        }
+        for (final Class<?> ifce : delegate.getInterfaces()) {
+            addNullDelegate(ifce);
+        }
+    }
+
     protected void applyAnnotations(final Collection<Annotation> annotations,
                                     final GeckoSessionSettings settings) {
         for (final Annotation annotation : annotations) {
             if (TimeoutMillis.class.equals(annotation.annotationType())) {
                 // Scale timeout based on the default timeout to account for the device under test.
                 final long value = ((TimeoutMillis) annotation).value();
                 final long timeout = value * getDefaultTimeoutMillis() / DEFAULT_TIMEOUT_MILLIS;
                 mTimeoutMillis = Math.max(timeout, 1000);
             } else if (Setting.class.equals(annotation.annotationType())) {
                 ((Setting) annotation).key().set(settings, ((Setting) annotation).value());
             } else if (Setting.List.class.equals(annotation.annotationType())) {
                 for (final Setting setting : ((Setting.List) annotation).value()) {
                     setting.key().set(settings, setting.value());
                 }
+            } else if (NullDelegate.class.equals(annotation.annotationType())) {
+                addNullDelegate(((NullDelegate) annotation).value());
+            } else if (NullDelegate.List.class.equals(annotation.annotationType())) {
+                for (final NullDelegate nullDelegate : ((NullDelegate.List) annotation).value()) {
+                    addNullDelegate(nullDelegate.value());
+                }
             } else if (WithDisplay.class.equals(annotation.annotationType())) {
                 final WithDisplay displaySize = (WithDisplay)annotation;
                 mDisplaySize = new Point(displaySize.width(), displaySize.height());
             } else if (ClosedSessionAtStart.class.equals(annotation.annotationType())) {
                 mClosedSession = ((ClosedSessionAtStart) annotation).value();
             }
         }
     }
@@ -706,16 +746,17 @@ public class GeckoSessionTestRule extend
         return env.isEmulator() ? DEFAULT_ARM_EMULATOR_TIMEOUT_MILLIS
                                 : DEFAULT_ARM_DEVICE_TIMEOUT_MILLIS;
     }
 
     protected void prepareStatement(final Description description) throws Throwable {
         final GeckoSessionSettings settings = new GeckoSessionSettings(mDefaultSettings);
         mTimeoutMillis = env.isDebugging() ? DEFAULT_IDE_DEBUG_TIMEOUT_MILLIS
                                            : getDefaultTimeoutMillis();
+        mNullDelegates = new HashSet<>();
         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();
@@ -793,17 +834,17 @@ public class GeckoSessionTestRule extend
 
         if (!mClosedSession) {
             openSession(mMainSession);
         }
     }
 
     protected void prepareSession(final GeckoSession session) throws Throwable {
         for (final Class<?> cls : CALLBACK_CLASSES) {
-            if (cls != null) {
+            if (!mNullDelegates.contains(cls)) {
                 getCallbackSetter(cls).invoke(session, mCallbackProxy);
             }
         }
     }
 
     /**
      * 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.
@@ -817,23 +858,32 @@ public class GeckoSessionTestRule extend
 
     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 {
+            // We cannot detect initial page load without progress delegate.
+            assertThat("ProgressDelegate cannot be null-delegate when opening session",
+                       GeckoSession.ProgressDelegate.class, not(isIn(mNullDelegates)));
+
+            // If navigation delegate is a null-delegate, instead of looking for
+            // onLocationChange(), start with the first call that targets this session.
+            final boolean nullNavigation = mNullDelegates.contains(
+                    GeckoSession.NavigationDelegate.class);
+
             mCallRecordHandler = new CallRecordHandler() {
                 private boolean mFoundStart = false;
 
                 @Override
                 public boolean handleCall(final Method method, final Object[] args) {
-                    if (!mFoundStart && sOnLocationChange.equals(method) &&
-                            session.equals(args[0]) && "about:blank".equals(args[1])) {
+                    if (!mFoundStart && session.equals(args[0]) && (nullNavigation ||
+                            (sOnLocationChange.equals(method) && "about:blank".equals(args[1])))) {
                         mFoundStart = true;
                         return true;
                     } else if (mFoundStart && session.equals(args[0])) {
                         if (sOnPageStop.equals(method)) {
                             mCallRecordHandler = null;
                         }
                         return true;
                     }
@@ -877,16 +927,17 @@ public class GeckoSessionTestRule extend
             mDisplaySurface.release();
             mDisplaySurface = null;
             mDisplayTexture.release();
             mDisplayTexture = null;
         }
 
         mMainSession = null;
         mCallbackProxy = null;
+        mNullDelegates = null;
         mCallRecords = null;
         mWaitScopeDelegates = null;
         mTestScopeDelegates = null;
         mLastWaitStart = 0;
         mLastWaitEnd = 0;
         mTimeoutMillis = 0;
     }
 
@@ -1154,17 +1205,17 @@ 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(session, callbackMethod,
+                    methodCalls.add(new MethodCall(session, method,
                                                    ac, /* target */ null));
                 }
             }
         }
 
         waitUntilCalled(session, callback.getClass(), methodCalls);
         forCallbacksDuringWait(session, callback);
     }
@@ -1183,21 +1234,39 @@ public class GeckoSessionTestRule extend
         for (final Class<?> ifce : CALLBACK_CLASSES) {
             final Object callback;
             try {
                 callback = getCallbackGetter(ifce).invoke(session == null ? mMainSession : session);
             } catch (final NoSuchMethodException | IllegalAccessException |
                     InvocationTargetException e) {
                 throw unwrapRuntimeException(e);
             }
+            if (mNullDelegates.contains(ifce)) {
+                // Null-delegates are initially null but are allowed to be any value.
+                continue;
+            }
             assertThat(ifce.getSimpleName() + " callbacks should be " +
                        "accessed through GeckoSessionTestRule delegate methods",
                        callback, sameInstance(mCallbackProxy));
         }
 
+        if (methodCalls.isEmpty()) {
+            // Waiting for any call on `delegate`; make sure it doesn't contain any null-delegates.
+            for (final Class<?> ifce : mNullDelegates) {
+                assertThat("Cannot wait on null-delegate callbacks",
+                           delegate, not(typeCompatibleWith(ifce)));
+            }
+        } else {
+            // Waiting for particular calls; make sure those calls aren't from a null-delegate.
+            for (final MethodCall call : methodCalls) {
+                assertThat("Cannot wait on null-delegate callbacks",
+                           call.method.getDeclaringClass(), not(isIn(mNullDelegates)));
+            }
+        }
+
         boolean calledAny = false;
         int index = mLastWaitStart = mLastWaitEnd;
 
         while (!calledAny || !methodCalls.isEmpty()) {
             while (index >= mCallRecords.size()) {
                 loopUntilIdle(mTimeoutMillis);
             }
 
@@ -1245,34 +1314,52 @@ public class GeckoSessionTestRule extend
      * @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 @Nullable GeckoSession session,
                                        final @NonNull Object callback) {
         final Method[] declaredMethods = callback.getClass().getDeclaredMethods();
         final List<MethodCall> methodCalls = new ArrayList<>(declaredMethods.length);
+        boolean assertingAnyCall = true;
+        Class<?> foundNullDelegate = null;
+
         for (final Class<?> ifce : CALLBACK_CLASSES) {
             if (!ifce.isInstance(callback)) {
                 continue;
             }
+            if (mNullDelegates.contains(ifce)) {
+                foundNullDelegate = ifce;
+            }
             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(
+                final MethodCall call = new MethodCall(
                         session, callbackMethod, getAssertCalled(callbackMethod, callback),
-                        /* target */ null));
+                        /* target */ null);
+                methodCalls.add(call);
+
+                if (call.requirement != null) {
+                    if (foundNullDelegate == ifce) {
+                        fail("Cannot assert on null-delegate " + ifce.getSimpleName());
+                    }
+                    assertingAnyCall = false;
+                }
             }
         }
 
+        if (assertingAnyCall && foundNullDelegate != null) {
+            fail("Cannot assert on null-delegate " + foundNullDelegate.getSimpleName());
+        }
+
         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) ||
                     (session != null && record.args[0] != session)) {
                 continue;