Bug 1456543 - 1. Add @NullDelegate test annotation; r=snorp
authorJim Chen <nchen@mozilla.com>
Fri, 27 Apr 2018 11:57:13 -0400
changeset 472177 429200bd5119b226cbede21ed258fefefe8b328e
parent 472176 c3163bee30b9be0e6224930e5c78327e2f4eaaf9
child 472178 206d01e57d0dfefec680d0a0f29254f700f8cc83
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp
bugs1456543
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 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;