--- 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>
* @Setting.List(@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;