Bug 1461746 - 3. Add Promises API support in RDP client; r=snorp

Add support for monitoring Promises, so tests can wait on Promises that
content returns. This makes it a lot easier to test Promise-based Web
APIs such as getUserMedia.

MozReview-Commit-ID: CHbeB7ErJgs

--HG--
extra : rebase_source : ccb3e8f4523ebc11389bdafafc16045d8c9fe50e
This commit is contained in:
Jim Chen 2018-05-15 13:15:12 -04:00
parent bbb5f532eb
commit f174b6c28a
4 changed files with 251 additions and 6 deletions

View File

@ -23,7 +23,7 @@ import java.util.Set;
* Provide methods for interacting with grips, including unpacking grips into Java
* objects.
*/
/* package */ final class Grip extends Actor {
public class Grip extends Actor {
private static final class Cache extends HashMap<String, Object> {
}
@ -159,6 +159,22 @@ import java.util.Set;
}
}
private static final class LongString {
private final int mLength;
private final String mInitial;
public LongString(final int length, final @Nullable String initial) {
mLength = length;
mInitial = (initial != null && !initial.isEmpty()) ? initial.substring(0, 50) : null;
}
@Override
public String toString() {
return String.format("[String(%d)]%s", mLength,
(mInitial != null) ? "(" + mInitial + "\u2026)" : "");
}
}
/**
* Unpack a received grip value into a Java object. The grip can be either a primitive
* value, or a JSONObject that represents a live object on the server.
@ -166,8 +182,8 @@ import java.util.Set;
* @param connection Connection associated with this grip.
* @param value Grip value received from the server.
*/
public static Object unpack(final RDPConnection connection,
final Object value) {
/* package */ static Object unpack(final @NonNull RDPConnection connection,
final @Nullable Object value) {
return unpackGrip(new Cache(), connection, value);
}
@ -181,7 +197,8 @@ import java.util.Set;
}
final JSONObject obj = (JSONObject) value;
switch (obj.optString("type")) {
final String type = obj.optString("type");
switch (type) {
case "null":
case "undefined":
return null;
@ -193,10 +210,12 @@ import java.util.Set;
return Double.NaN;
case "-0":
return -0.0;
case "longString":
return new LongString(obj.optInt("length"), obj.optString("initial"));
case "object":
break;
default:
throw new IllegalArgumentException();
throw new IllegalArgumentException(String.valueOf(type));
}
final String actor = obj.optString("actor", null);
@ -213,6 +232,10 @@ import java.util.Set;
final Function output = new Function(userDisplayName);
cache.put(actor, output);
return output;
} else if ("Promise".equals(cls)) {
final Promise output = new Promise(connection, obj);
cache.put(actor, output);
return output;
}
final JSONObject preview = obj.optJSONObject("preview");
@ -249,7 +272,7 @@ import java.util.Set;
}
};
private Grip(final RDPConnection connection, final JSONObject grip) {
/* package */ Grip(final @NonNull RDPConnection connection, final @NonNull JSONObject grip) {
super(connection, grip);
}

View File

@ -0,0 +1,111 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.geckoview.test.rdp;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.json.JSONObject;
/**
* Object to represent a Promise object returned by JS.
*/
public final class Promise extends Grip {
private JSONObject mGrip;
private String mState;
private Object mValue;
private Object mReason;
/* package */ Promise(final @NonNull RDPConnection connection, final @NonNull JSONObject grip) {
super(connection, grip);
setPromiseState(grip);
}
/* package */ void setPromiseState(final @NonNull JSONObject grip) {
mGrip = grip;
final JSONObject state = grip.optJSONObject("promiseState");
mState = state.optString("state");
if (isFulfilled()) {
mValue = Grip.unpack(connection, state.opt("value"));
} else if (isRejected()) {
mReason = Grip.unpack(connection, state.opt("reason"));
}
}
/**
* Return whether this promise is pending.
*
* @return True if this promise is pending.
*/
public boolean isPending() {
return "pending".equals(mState);
}
/**
* Return whether this promise is fulfilled.
*
* @return True if this promise is fulfilled.
*/
public boolean isFulfilled() {
return "fulfilled".equals(mState);
}
/**
* Return the promise value, assuming it is fulfilled.
*
* @return Promise value.
*/
public @Nullable Object getValue() {
return mValue;
}
/**
* Return whether this promise is rejected.
*
* @return True if this promise is rejected.
*/
public boolean isRejected() {
return "rejected".equals(mState);
}
/**
* Return the promise reason, assuming it is rejected.
*
* @return Promise reason.
*/
public @Nullable Object getReason() {
return mReason;
}
/**
* Get the value of a property in this promise object.
*
* @return Value or null if there is no such property.
*/
public @Nullable Object getProperty(final @NonNull String name) {
final JSONObject preview = mGrip.optJSONObject("preview");
if (preview == null) {
return null;
}
final JSONObject ownProperties = preview.optJSONObject("ownProperties");
if (ownProperties == null) {
return null;
}
final JSONObject prop = ownProperties.optJSONObject(name);
if (prop == null) {
return null;
}
return Grip.unpack(connection, prop.opt("value"));
}
@Override
public String toString() {
return "[Promise(" + mState + ")]" +
(isFulfilled() ? "(" + mValue + ')' : "") +
(isRejected() ? "(" + mReason + ')' : "");
}
}

View File

@ -0,0 +1,100 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.geckoview.test.rdp;
import android.support.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Provide access to the promises API.
*/
public final class Promises extends Actor {
private final ReplyParser<Promise[]> PROMISE_LIST_PARSER = new ReplyParser<Promise[]>() {
@Override
public boolean canParse(@NonNull JSONObject packet) {
return packet.has("promises");
}
@Override
public @NonNull Promise[] parse(@NonNull JSONObject packet) {
return getPromisesFromArray(packet.optJSONArray("promises"),
/* canCreate */ true);
}
};
private final Set<Promise> mPromises = new HashSet<>();
/* package */ Promises(final RDPConnection connection, final String name) {
super(connection, name);
attach();
}
/**
* Attach to the promises API.
*/
private void attach() {
sendPacket("{\"type\":\"attach\"}", JSON_PARSER).get();
}
/**
* Detach from the promises API.
*/
public void detach() {
for (final Promise promise : mPromises) {
promise.release();
}
sendPacket("{\"type\":\"detach\"}", JSON_PARSER).get();
}
/* package */ Promise[] getPromisesFromArray(final @NonNull JSONArray array,
final boolean canCreate) {
final Promise[] promises = new Promise[array.length()];
for (int i = 0; i < promises.length; i++) {
final JSONObject grip = array.optJSONObject(i);
final Promise promise = (Promise) connection.getActor(grip);
if (promise != null) {
promise.setPromiseState(grip);
promises[i] = promise;
} else if (canCreate) {
promises[i] = new Promise(connection, grip);
}
}
return promises;
}
/**
* Return a list of live promises.
*
* @returns List of promises.
*/
public @NonNull Promise[] listPromises() {
final Promise[] promises = sendPacket("{\"type\":\"listPromises\"}",
PROMISE_LIST_PARSER).get();
mPromises.addAll(Arrays.asList(promises));
return promises;
}
@Override
protected void onPacket(final @NonNull JSONObject packet) {
final String type = packet.optString("type", null);
if ("new-promises".equals(type)) {
// We always call listPromises() to get updated Promises,
// so that means we shouldn't handle "new-promises" here.
} else if ("promises-settled".equals(type)) {
// getPromisesFromArray will update states for us.
getPromisesFromArray(packet.optJSONArray("data"),
/* canCreate */ false);
} else {
super.onPacket(packet);
}
}
}

View File

@ -64,4 +64,15 @@ public final class Tab extends Actor {
final Actor console = connection.getActor(name);
return (console != null) ? (Console) console : new Console(connection, name);
}
/**
* Get the promises object for access to the promises API.
*
* @return Promises object.
*/
public Promises getPromises() {
final String name = mTab.optString("promisesActor", null);
final Actor promises = connection.getActor(name);
return (promises != null) ? (Promises) promises : new Promises(connection, name);
}
}