Bug 1473996 - Expose getPropertyValue in devtools server to fully evaluate an object property. r=nchevobbe

Differential Revision: https://phabricator.services.mozilla.com/D6722

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Logan F Smyth 2018-09-26 16:12:56 +00:00
Родитель 022d2224d5
Коммит 11febfa688
7 изменённых файлов: 353 добавлений и 1 удалений

Просмотреть файл

@ -507,6 +507,53 @@ const proto = {
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.obj.getProperty(name);
return { value: this._buildCompletion(value) };
},
/**
* 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.
*/

Просмотреть файл

@ -1610,7 +1610,13 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
getGripDepth: () => this._gripDepth,
incrementGripDepth: () => this._gripDepth++,
decrementGripDepth: () => this._gripDepth--,
createValueGrip: v => createValueGrip(v, this._pausePool, this.pauseObjectGrip),
createValueGrip: v => {
if (this._pausePool) {
return createValueGrip(v, this._pausePool, this.pauseObjectGrip);
}
return createValueGrip(v, this.threadLifetimePool, this.objectGrip);
},
sources: () => this.sources,
createEnvironmentActor: (e, p) => this.createEnvironmentActor(e, p),
promote: () => this.threadObjectGrip(actor),

Просмотреть файл

@ -0,0 +1,183 @@
/* 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 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_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);
}
}

Просмотреть файл

@ -0,0 +1,84 @@
/* 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({
get prop(){
debugger;
},
});
`;
const objClient = await eval_and_resume(debuggee, threadClient, code, async frame => {
const arg1 = frame.arguments[0];
Assert.equal(arg1.class, "Object");
const obj = threadClient.pauseGrip(arg1);
await obj.threadGrip();
return obj;
});
// 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);
}),
objClient.getPropertyValue("prop"),
]);
}
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);
});
});
}

Просмотреть файл

@ -177,6 +177,8 @@ reason = bug 1104838
[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_promise_state-01.js]
[test_promise_state-02.js]
[test_promise_state-03.js]

Просмотреть файл

@ -42,6 +42,10 @@ ObjectClient.prototype = {
return this._grip.extensible;
},
threadGrip: DebuggerClient.requester({
type: "threadGrip",
}),
getDefinitionSite: DebuggerClient.requester({
type: "definitionSite"
}, {
@ -181,6 +185,17 @@ ObjectClient.prototype = {
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.
*

Просмотреть файл

@ -24,6 +24,11 @@ types.addDictType("object.descriptor", {
set: "nullable:json",
});
types.addDictType("object.completion", {
return: "nullable:json",
throw: "nullable:json"
});
types.addDictType("object.definitionSite", {
source: "source",
line: "number",
@ -45,6 +50,10 @@ 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",
@ -165,6 +174,12 @@ const objectSpec = generateActorSpec({
},
response: RetVal("object.property")
},
propertyValue: {
request: {
name: Arg(0, "string")
},
response: RetVal("object.propertyValue")
},
rejectionStack: {
request: {},
response: {