author | Logan F Smyth <loganfsmyth@gmail.com> |
Wed, 26 Sep 2018 16:23:25 +0000 | |
changeset 438318 | 8c9d2be6d47e0a530e161430f06f47678c25fe3f |
parent 438317 | df8f12fd43e7829fd9b96e96e0ae063b6d971fe2 |
child 438319 | 513ebcc0f395531d2bfea60154515dee8c7987d0 |
push id | 34716 |
push user | shindli@mozilla.com |
push date | Wed, 26 Sep 2018 21:51:41 +0000 |
treeherder | mozilla-central@f8b2114ab512 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | nchevobbe |
bugs | 1473996 |
milestone | 64.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
|
--- a/devtools/server/actors/object.js +++ b/devtools/server/actors/object.js @@ -525,16 +525,61 @@ const proto = { } const value = this.obj.getProperty(name); return { value: this._buildCompletion(value) }; }, /** + * 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) { + if (!this.obj.callable) { + return this.throwError("notCallable", "debugee object is not callable"); + } + + const debugeeContext = this._getValueFromGrip(context); + const debugeeArgs = args && args.map(this._getValueFromGrip, this); + + 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-01.js @@ -0,0 +1,154 @@ +/* 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 eval_and_resume(debuggee, threadClient, code, async frame => { + const arg1 = frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + await objectHandler(threadClient.pauseGrip(arg1)); + }); +} + +function eval_and_resume(debuggee, threadClient, code, callback) { + return new Promise((resolve, reject) => { + wait_for_pause(threadClient, callback).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 wait_for_pause(threadClient, callback = () => {}) { + return new Promise((resolve, reject) => { + threadClient.addOneTimeListener("paused", function(event, packet) { + (async () => { + try { + return await callback(packet.frame); + } finally { + await threadClient.resume(); + } + })().then(resolve, reject); + }); + }); +} + +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); + } +}
new file mode 100644 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-fn-apply-02.js @@ -0,0 +1,88 @@ +/* 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) { + const code = ` + stopMe({ + method(){ + debugger; + }, + }); + `; + const obj = await eval_and_resume(debuggee, threadClient, code, async frame => { + const arg1 = frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + await threadClient.pauseGrip(arg1).threadGrip(); + return arg1; + }); + const objClient = threadClient.pauseGrip(obj); + + const method = threadClient.pauseGrip( + (await objClient.getPropertyValue("method")).value.return, + ); + + // Ensure that we actually paused at the `debugger;` line. + await Promise.all([ + wait_for_pause(threadClient, frame => { + Assert.equal(frame.where.line, 4); + Assert.equal(frame.where.column, 8); + }), + method.apply(obj, []), + ]); +} + +function eval_and_resume(debuggee, threadClient, code, callback) { + return new Promise((resolve, reject) => { + wait_for_pause(threadClient, callback).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 wait_for_pause(threadClient, callback = () => {}) { + return new Promise((resolve, reject) => { + threadClient.addOneTimeListener("paused", function(event, packet) { + (async () => { + try { + return await callback(packet.frame); + } finally { + await threadClient.resume(); + } + })().then(resolve, reject); + }); + }); +}
new file mode 100644 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-fn-apply-03.js @@ -0,0 +1,87 @@ +/* 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); + + // TODO: The server currently doesn't handle request errors + // in workers well, so this test doesn't work with the worker server. + // 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) { + const code = ` + stopMe({ + method: {}, + }); + `; + const obj = await eval_and_resume(debuggee, threadClient, code, async frame => { + const arg1 = frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + await threadClient.pauseGrip(arg1).threadGrip(); + return arg1; + }); + const objClient = threadClient.pauseGrip(obj); + + const method = threadClient.pauseGrip( + (await objClient.getPropertyValue("method")).value.return, + ); + + try { + await method.apply(obj, []); + Assert.ok(false, "expected exception"); + } catch (err) { + Assert.equal(err.message, "debugee object is not callable"); + } +} + +function eval_and_resume(debuggee, threadClient, code, callback) { + return new Promise((resolve, reject) => { + wait_for_pause(threadClient, callback).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 wait_for_pause(threadClient, callback = () => {}) { + return new Promise((resolve, reject) => { + threadClient.addOneTimeListener("paused", function(event, packet) { + (async () => { + try { + return await callback(packet.frame); + } finally { + await threadClient.resume(); + } + })().then(resolve, reject); + }); + }); +}
--- a/devtools/server/tests/unit/xpcshell.ini +++ b/devtools/server/tests/unit/xpcshell.ini @@ -174,16 +174,19 @@ reason = bug 1104838 [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-property-value-01.js] [test_objectgrips-property-value-02.js] +[test_objectgrips-fn-apply-01.js] +[test_objectgrips-fn-apply-02.js] +[test_objectgrips-fn-apply-03.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] [test_stepping-04.js]
--- a/devtools/shared/client/object-client.js +++ b/devtools/shared/client/object-client.js @@ -201,16 +201,29 @@ ObjectClient.prototype = { * * @param onResponse function Called with the request's response. */ getPrototype: DebuggerClient.requester({ type: "prototype" }), /** + * Evaluate a callable object with context and arguments. + * + * @param context any The value to use as the function context. + * @param arguments Array<any> An array of values to use as the function's arguments. + * @param onResponse function Called with the request's response. + */ + apply: DebuggerClient.requester({ + type: "apply", + context: 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,19 +49,23 @@ 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", + variables: "json" }); types.addDictType("object.scope", { scope: "environment" }); types.addDictType("object.enumProperties.Options", { enumEntries: "nullable:boolean", @@ -175,16 +179,23 @@ const objectSpec = generateActorSpec({ response: RetVal("object.property") }, propertyValue: { request: { name: Arg(0, "string") }, response: RetVal("object.propertyValue") }, + apply: { + request: { + context: 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: {