☠☠ backed out by be97edb6e793 ☠ ☠ | |
author | Logan F Smyth <loganfsmyth@gmail.com> |
Thu, 12 Jul 2018 11:08:38 -0700 | |
changeset 427013 | 9ba8a6f1857d16d3389c81f3323df1149ae94841 |
parent 427012 | 162af260bace9f34f633ae743a748e415490b03f |
child 427014 | dc601e6050f67f30980aabbf13e179790f4a4485 |
push id | 66490 |
push user | nchevobbe@mozilla.com |
push date | Wed, 18 Jul 2018 05:48:57 +0000 |
treeherder | autoland@dc601e6050f6 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | nchevobbe |
bugs | 1473996 |
milestone | 63.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 @@ -23,16 +23,18 @@ const { getArrayLength, getPromiseState, getStorageLength, isArray, isStorage, isTypedArray, } = require("devtools/server/actors/object/utils"); +const propertyValueGettersMap = new WeakMap(); + const proto = { /** * Creates an actor for the specified object. * * @param obj Debugger.Object * The debuggee object. * @param Object * A collection of abstract methods that are implemented by the caller. @@ -486,16 +488,85 @@ const proto = { if (!name) { return this.throwError("missingParameter", "no property name was specified"); } return { descriptor: this._propertyDescriptor(name) }; }, /** + * Handle a protocol request to provide the value of the object's + * specified property. + * + * Note: Since this will evaluate getters, 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 {string} name + * The property we want the value of. + */ + propertyValue: function(name) { + if (!name) { + return this.throwError("missingParameter", "no property name was specified"); + } + + const value = this._getPropertyGetter()(this.obj, name); + + return { value: this._buildCompletion(value) }; + }, + + /** + * Rather than re-implement the logic for looking up the property of an + * object, this utility allows for easily generating a content function + * that can perform that lookup. + */ + _getPropertyGetter() { + const { global } = this.obj; + let getter = propertyValueGettersMap.get(global); + if (getter) { + return getter; + } + + const debugeeGetter = global.executeInGlobal("((obj, key) => obj[key]);").return; + getter = (obj, key) => { + // eslint-disable-next-line no-useless-call + return debugeeGetter.call(undefined, obj, key); + }; + propertyValueGettersMap.set(global, getter); + + return getter; + }, + + /** + * 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; + + // .apply result will be falsy if the script being executed is terminated + // via the "slow script" dialog. + if (value) { + completionGrip = {}; + if ("return" in value) { + completionGrip.return = this.hooks.createValueGrip(value.return); + } + if ("throw" in value) { + completionGrip.throw = this.hooks.createValueGrip(value.throw); + } + } + + return completionGrip; + }, + + /** * Handle a protocol request to provide the display string for the object. */ displayString: function() { const string = stringify(this.obj); return { displayString: this.hooks.createValueGrip(string) }; }, /**
new file mode 100644 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-property-value.js @@ -0,0 +1,171 @@ +/* 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, + ` + var obj = { + stringProp: "a value", + get stringNormal(){ + return "a value"; + }, + get stringAbrupt() { + throw "a value"; + }, + get objectNormal() { + return { prop: 4 }; + }, + get objectAbrupt() { + throw { prop: 4 }; + }, + get context(){ + return this === obj ? "correct context" : "wrong context"; + }, + method() { + return "a value"; + }, + }; + stopMe(obj); + `, + async objClient => { + const expectedValues = { + stringProp: { + return: "a value", + }, + stringNormal: { + return: "a value", + }, + stringAbrupt: { + throw: "a value", + }, + objectNormal: { + return: { + type: "object", + class: "Object", + ownPropertyLength: 1, + preview: { + kind: "Object", + ownProperties: { + prop: { + value: 4, + }, + }, + }, + }, + }, + objectAbrupt: { + throw: { + type: "object", + class: "Object", + ownPropertyLength: 1, + preview: { + kind: "Object", + ownProperties: { + prop: { + value: 4, + }, + }, + }, + }, + }, + context: { + return: "correct context", + }, + method: { + return: { + type: "object", + class: "Function", + name: "method", + }, + }, + }; + + for (const [key, expected] of Object.entries(expectedValues)) { + const { value } = await objClient.getPropertyValue(key); + + assert_completion(value, expected); + } + }, + ); +} + +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_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-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] [test_stepping-04.js]
--- a/devtools/shared/client/object-client.js +++ b/devtools/shared/client/object-client.js @@ -177,16 +177,27 @@ ObjectClient.prototype = { * @param onResponse function Called with the request's response. */ getProperty: DebuggerClient.requester({ type: "property", name: arg(0) }), /** + * 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. + */ + getPropertyValue: DebuggerClient.requester({ + type: "propertyValue", + name: arg(0) + }), + + /** * Request the prototype of the object. * * @param onResponse function Called with the request's response. */ getPrototype: DebuggerClient.requester({ type: "prototype" }),
--- a/devtools/shared/specs/object.js +++ b/devtools/shared/specs/object.js @@ -19,16 +19,21 @@ types.addDictType("object.descriptor", { // Only set `value` exists. writable: "nullable:boolean", // Only set when `value` does not exist and there is a getter for the property. get: "nullable:json", // Only set when `value` does not exist and there is a setter for the property. set: "nullable:json", }); +types.addDictType("object.completion", { + return: "nullable:json", + throw: "nullable:json" +}); + types.addDictType("object.definitionSite", { source: "source", line: "number", column: "number", }); types.addDictType("object.prototypeproperties", { prototype: "object.descriptor", @@ -40,16 +45,20 @@ types.addDictType("object.prototypeprope types.addDictType("object.prototype", { prototype: "object.descriptor", }); types.addDictType("object.property", { descriptor: "nullable:object.descriptor" }); +types.addDictType("object.propertyValue", { + value: "nullable:object.completion" +}); + types.addDictType("object.bindings", { arguments: "array:json", variables: "json", }); types.addDictType("object.scope", { scope: "environment" }); @@ -160,16 +169,22 @@ const objectSpec = generateActorSpec({ response: RetVal("object.prototype") }, property: { request: { name: Arg(0, "string") }, response: RetVal("object.property") }, + propertyValue: { + request: { + name: Arg(0, "string") + }, + response: RetVal("object.propertyValue") + }, rejectionStack: { request: {}, response: { rejectionStack: RetVal("array:object.originalSourceLocation") }, }, release: { release: true }, scope: {