Bug 1473996 - Expose fn.apply in the devtools server. r=nchevobbe
MozReview-Commit-ID: 33Uh8UIuz4g
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -532,16 +532,61 @@ const proto = {
return debugeeGetter.call(undefined, obj, key);
};
propertyValueGettersMap.set(global, getter);
return getter;
},
/**
+ * Handle a protocol request to evaluate a function and provide the value of
+ * the result.
+ *
+ * Note: Since this will evaluate the function, it can trigger execution of
+ * content code and may cause side effects. This endpoint should only be used
+ * when you are confident that the side-effects will be safe, or the user
+ * is expecting the effects.
+ *
+ * @param {any} context
+ * The 'this' value to call the function with.
+ * @param {Array<any>} args
+ * The array of un-decoded actor objects, or primitives.
+ */
+ apply: function(context, args) {
+ const debugeeContext = this._getValueFromGrip(context);
+ const debugeeArgs = args && args.map(this._getValueFromGrip, this);
+
+ if (!this.obj.callable) {
+ return this.throwError("notCallable", "debugee object is not callable");
+ }
+
+ const value = this.obj.apply(debugeeContext, debugeeArgs);
+
+ return { value: this._buildCompletion(value) };
+ },
+
+ _getValueFromGrip(grip) {
+ if (typeof grip !== "object" || !grip) {
+ return grip;
+ }
+
+ if (typeof grip.actor !== "string") {
+ return this.throwError("invalidGrip", "grip argument did not include actor ID");
+ }
+
+ const actor = this.conn.getActor(grip.actor);
+
+ if (!actor) {
+ return this.throwError("unknownActor", "grip actor did not match a known object");
+ }
+
+ return actor.obj;
+ },
+
+ /**
* Converts a Debugger API completion value record into an eqivalent
* object grip for use by the API.
*
* See https://developer.mozilla.org/en-US/docs/Tools/Debugger-API/Conventions#completion-values
* for more specifics on the expected behavior.
*/
_buildCompletion(value) {
let completionGrip = null;
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_objectgrips-fn-apply.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+async function run_test() {
+ try {
+ do_test_pending();
+ await run_test_with_server(DebuggerServer);
+ await run_test_with_server(WorkerDebuggerServer);
+ } finally {
+ do_test_finished();
+ }
+}
+
+async function run_test_with_server(server) {
+ initTestDebuggerServer(server);
+ const debuggee = addTestGlobal("test-grips", server);
+ debuggee.eval(`
+ function stopMe(arg1) {
+ debugger;
+ }
+ `);
+
+ const dbgClient = new DebuggerClient(server.connectPipe());
+ await dbgClient.connect();
+ const [,, threadClient] = await attachTestTabAndResume(dbgClient, "test-grips");
+
+ await test_object_grip(debuggee, threadClient);
+
+ await dbgClient.close();
+}
+
+async function test_object_grip(debuggee, threadClient) {
+ await assert_object_argument(
+ debuggee,
+ threadClient,
+ `
+ stopMe({
+ obj1: {},
+ obj2: {},
+ context(arg) {
+ return this === arg ? "correct context" : "wrong context";
+ },
+ sum(...parts) {
+ return parts.reduce((acc, v) => acc + v, 0);
+ },
+ error() {
+ throw "an error";
+ },
+ });
+ `,
+ async objClient => {
+ const obj1 = (await objClient.getPropertyValue("obj1")).value.return;
+ const obj2 = (await objClient.getPropertyValue("obj2")).value.return;
+
+ const context = threadClient.pauseGrip(
+ (await objClient.getPropertyValue("context")).value.return,
+ );
+ const sum = threadClient.pauseGrip(
+ (await objClient.getPropertyValue("sum")).value.return,
+ );
+ const error = threadClient.pauseGrip(
+ (await objClient.getPropertyValue("error")).value.return,
+ );
+
+ assert_response(await context.apply(obj1, [obj1]), {
+ return: "correct context",
+ });
+ assert_response(await context.apply(obj2, [obj2]), {
+ return: "correct context",
+ });
+ assert_response(await context.apply(obj1, [obj2]), {
+ return: "wrong context",
+ });
+ assert_response(await context.apply(obj2, [obj1]), {
+ return: "wrong context",
+ });
+ // eslint-disable-next-line no-useless-call
+ assert_response(await sum.apply(null, [1, 2, 3, 4, 5, 6, 7]), {
+ return: 1 + 2 + 3 + 4 + 5 + 6 + 7,
+ });
+ // eslint-disable-next-line no-useless-call
+ assert_response(await error.apply(null, []), {
+ throw: "an error",
+ });
+ },
+ );
+}
+
+function assert_object_argument(debuggee, threadClient, code, objectHandler) {
+ return new Promise((resolve, reject) => {
+ threadClient.addOneTimeListener("paused", function(event, packet) {
+ (async () => {
+ try {
+ const arg1 = packet.frame.arguments[0];
+ Assert.equal(arg1.class, "Object");
+
+ await objectHandler(threadClient.pauseGrip(arg1));
+ } finally {
+ await threadClient.resume();
+ }
+ })().then(resolve, reject);
+ });
+
+ // This synchronously blocks until 'threadClient.resume()' above runs
+ // because the 'paused' event runs everthing in a new event loop.
+ debuggee.eval(code);
+ });
+}
+
+function assert_response({ value }, expected) {
+ assert_completion(value, expected);
+}
+
+function assert_completion(value, expected) {
+ if (expected && "return" in expected) {
+ assert_value(value.return, expected.return);
+ }
+ if (expected && "throw" in expected) {
+ assert_value(value.throw, expected.throw);
+ }
+ if (!expected) {
+ assert_value(value, expected);
+ }
+}
+
+function assert_value(actual, expected) {
+ Assert.equal(typeof actual, typeof expected);
+
+ if (typeof expected === "object") {
+ // Note: We aren't using deepEqual here because we're only doing a cursory
+ // check of a few properties, not a full comparison of the result, since
+ // the full outputs includes stuff like preview info that we don't need.
+ for (const key of Object.keys(expected)) {
+ assert_value(actual[key], expected[key]);
+ }
+ } else {
+ Assert.equal(actual, expected);
+ }
+}
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -172,16 +172,17 @@ reason = bug 1104838
[test_objectgrips-16.js]
[test_objectgrips-17.js]
[test_objectgrips-18.js]
[test_objectgrips-19.js]
[test_objectgrips-20.js]
[test_objectgrips-21.js]
[test_objectgrips-22.js]
[test_objectgrips-array-like-object.js]
+[test_objectgrips-fn-apply.js]
[test_objectgrips-property-value.js]
[test_promise_state-01.js]
[test_promise_state-02.js]
[test_promise_state-03.js]
[test_interrupt.js]
[test_stepping-01.js]
[test_stepping-02.js]
[test_stepping-03.js]
--- a/devtools/shared/client/object-client.js
+++ b/devtools/shared/client/object-client.js
@@ -197,16 +197,28 @@ ObjectClient.prototype = {
*
* @param onResponse function Called with the request's response.
*/
getPrototype: DebuggerClient.requester({
type: "prototype"
}),
/**
+ * Request the value of the object's specified property.
+ *
+ * @param name string The name of the requested property.
+ * @param onResponse function Called with the request's response.
+ */
+ apply: DebuggerClient.requester({
+ type: "apply",
+ this: arg(0),
+ arguments: arg(1),
+ }),
+
+ /**
* Request the display string of the object.
*
* @param onResponse function Called with the request's response.
*/
getDisplayString: DebuggerClient.requester({
type: "displayString"
}),
--- a/devtools/shared/specs/object.js
+++ b/devtools/shared/specs/object.js
@@ -49,16 +49,20 @@ types.addDictType("object.prototype", {
types.addDictType("object.property", {
descriptor: "nullable:object.descriptor"
});
types.addDictType("object.propertyValue", {
value: "nullable:object.completion"
});
+types.addDictType("object.apply", {
+ value: "nullable:object.completion"
+});
+
types.addDictType("object.bindings", {
arguments: "array:json",
variables: "json",
});
types.addDictType("object.scope", {
scope: "environment"
});
@@ -175,16 +179,23 @@ const objectSpec = generateActorSpec({
response: RetVal("object.property")
},
propertyValue: {
request: {
name: Arg(0, "string")
},
response: RetVal("object.propertyValue")
},
+ apply: {
+ request: {
+ this: Arg(0, "nullable:json"),
+ arguments: Arg(1, "nullable:array:json"),
+ },
+ response: RetVal("object.apply")
+ },
rejectionStack: {
request: {},
response: {
rejectionStack: RetVal("array:object.originalSourceLocation")
},
},
release: { release: true },
scope: {