Bug 1392614 - Add an onEnumSymbols function on the object actor. r=ochameau
This will enable lazy loading symbols for a grip if needed.
This patch introduce a new SymbolIteratorActor, which is similar
to PropertyIteratorActor but with a data structure that better
fits symbols, and how we already handle them, i.e. an `ownSymbols`
array property (and not an `ownProperties` object like PropertyIteratorActor).
We take this as an opportunity to add a test for enumSymbols, but also
for enumProperties that did not have server unit tests (it is only tested
in the frontend with the variable view, which might be deprecated at some
point).
MozReview-Commit-ID: IEIKA8zwH90
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -258,18 +258,41 @@ ObjectActor.prototype = {
onEnumEntries: function () {
let actor = new PropertyIteratorActor(this, { enumEntries: true });
this.registeredPool.addActor(actor);
this.iterators.add(actor);
return { iterator: actor.grip() };
},
/**
+ * Creates an actor to iterate over an object symbols properties.
+ */
+ onEnumSymbols: function () {
+ let actor = new SymbolIteratorActor(this);
+ this.registeredPool.addActor(actor);
+ this.iterators.add(actor);
+ return { iterator: actor.grip() };
+ },
+
+ /**
* Handle a protocol request to provide the prototype and own properties of
* the object.
+ *
+ * @returns {Object} An object containing the data of this.obj, of the following form:
+ * - {string} from: this.obj's actorID.
+ * - {Object} prototype: The descriptor of this.obj's prototype.
+ * - {Object} ownProperties: an object where the keys are the names of the
+ * this.obj's ownProperties, and the values the descriptors of
+ * the properties.
+ * - {Array} ownSymbols: An array containing all descriptors of this.obj's
+ * ownSymbols. Here we have an array, and not an object like for
+ * ownProperties, because we can have multiple symbols with the same
+ * name in this.obj, e.g. `{[Symbol()]: "a", [Symbol()]: "b"}`.
+ * - {Object} safeGetterValues: an object that maps this.obj's property names
+ * with safe getters descriptors.
*/
onPrototypeAndProperties: function () {
let ownProperties = Object.create(null);
let ownSymbols = [];
let names;
let symbols;
try {
names = this.obj.getOwnPropertyNames();
@@ -746,16 +769,17 @@ ObjectActor.prototype.requestTypes = {
"decompile": ObjectActor.prototype.onDecompile,
"release": ObjectActor.prototype.onRelease,
"scope": ObjectActor.prototype.onScope,
"dependentPromises": ObjectActor.prototype.onDependentPromises,
"allocationStack": ObjectActor.prototype.onAllocationStack,
"fulfillmentStack": ObjectActor.prototype.onFulfillmentStack,
"rejectionStack": ObjectActor.prototype.onRejectionStack,
"enumEntries": ObjectActor.prototype.onEnumEntries,
+ "enumSymbols": ObjectActor.prototype.onEnumSymbols,
};
/**
* Creates an actor to iterate over an object's property names and values.
*
* @param objectActor ObjectActor
* The object actor.
* @param options Object
@@ -828,17 +852,17 @@ PropertyIteratorActor.prototype = {
ownProperties[name] = this.iterator.propertyDescription(i);
}
return {
ownProperties
};
},
all() {
- return this.slice({ start: 0, count: this.length });
+ return this.slice({ start: 0, count: this.iterator.size });
}
};
PropertyIteratorActor.prototype.requestTypes = {
"names": PropertyIteratorActor.prototype.names,
"slice": PropertyIteratorActor.prototype.slice,
"all": PropertyIteratorActor.prototype.all,
};
@@ -1121,16 +1145,68 @@ function enumWeakSetEntries(objectActor)
enumerable: true,
value: gripFromEntry(objectActor, val)
};
}
};
}
/**
+ * Creates an actor to iterate over an object's symbols.
+ *
+ * @param objectActor ObjectActor
+ * The object actor.
+ */
+function SymbolIteratorActor(objectActor) {
+ const symbols = objectActor.obj.getOwnPropertySymbols();
+
+ this.iterator = {
+ size: symbols.length,
+ symbolDescription(index) {
+ const symbol = symbols[index];
+ return {
+ name: symbol.toString(),
+ descriptor: objectActor._propertyDescriptor(symbol, true)
+ };
+ }
+ };
+}
+
+SymbolIteratorActor.prototype = {
+ actorPrefix: "symbolIterator",
+
+ grip() {
+ return {
+ type: this.actorPrefix,
+ actor: this.actorID,
+ count: this.iterator.size
+ };
+ },
+
+ slice({ start, count }) {
+ let ownSymbols = [];
+ for (let i = start, m = start + count; i < m; i++) {
+ ownSymbols.push(this.iterator.symbolDescription(i));
+ }
+ return {
+ ownSymbols
+ };
+ },
+
+ all() {
+ return this.slice({ start: 0, count: this.iterator.size });
+ }
+};
+
+SymbolIteratorActor.prototype.requestTypes = {
+ "slice": SymbolIteratorActor.prototype.slice,
+ "all": SymbolIteratorActor.prototype.all,
+};
+
+/**
* Functions for adding information to ObjectActor grips for the purpose of
* having customized output. This object holds arrays mapped by
* Debugger.Object.prototype.class.
*
* In each array you can add functions that take three
* arguments:
* - the ObjectActor instance and its hooks to make a preview for,
* - the grip object being prepared for the client,
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_objectgrips-18.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+var gCallback;
+
+function run_test() {
+ run_test_with_server(DebuggerServer, function () {
+ run_test_with_server(WorkerDebuggerServer, do_test_finished);
+ });
+ do_test_pending();
+}
+
+async function run_test_with_server(server, callback) {
+ gCallback = callback;
+ initTestDebuggerServer(server);
+ gDebuggee = addTestGlobal("test-grips", server);
+ gDebuggee.eval(function stopMe(arg1) {
+ debugger;
+ }.toString());
+
+ gClient = new DebuggerClient(server.connectPipe());
+ await gClient.connect();
+ const [,, threadClient] = await attachTestTabAndResume(gClient, "test-grips");
+ gThreadClient = threadClient;
+ test_object_grip();
+}
+
+async function test_object_grip() {
+ gThreadClient.addOneTimeListener("paused", async function (event, packet) {
+ let [grip] = packet.frame.arguments;
+
+ let objClient = gThreadClient.pauseGrip(grip);
+
+ // Checks the result of enumProperties.
+ let response = await objClient.enumProperties({});
+ await check_enum_properties(response);
+
+ // Checks the result of enumSymbols.
+ response = await objClient.enumSymbols();
+ await check_enum_symbols(response);
+
+ await gThreadClient.resume();
+ await gClient.close();
+ gCallback();
+ });
+
+ gDebuggee.eval(`
+ var obj = Array.from({length: 10})
+ .reduce((res, _, i) => {
+ res["property_" + i + "_key"] = "property_" + i + "_value";
+ res[Symbol("symbol_" + i)] = "symbol_" + i + "_value";
+ return res;
+ }, {});
+
+ obj[Symbol()] = "first unnamed symbol";
+ obj[Symbol()] = "second unnamed symbol";
+ obj[Symbol.iterator] = function* () {
+ yield 1;
+ yield 2;
+ };
+
+ stopMe(obj);
+ `);
+}
+
+async function check_enum_properties(response) {
+ do_print("Check enumProperties response");
+ ok(response && Object.getOwnPropertyNames(response).includes("iterator"),
+ "The response object has an iterator property");
+
+ const {iterator} = response;
+ equal(iterator.count, 10, "iterator.count has the expected value");
+
+ do_print("Check iterator.slice response for all properties");
+ let sliceResponse = await iterator.slice(0, iterator.count);
+ ok(sliceResponse && Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"),
+ "The response object has an ownProperties property");
+
+ let {ownProperties} = sliceResponse;
+ let names = Object.keys(ownProperties);
+ equal(names.length, iterator.count,
+ "The response has the expected number of properties");
+ for (let i = 0; i < names.length; i++) {
+ const name = names[i];
+ equal(name, `property_${i}_key`);
+ equal(ownProperties[name].value, `property_${i}_value`);
+ }
+
+ do_print("Check iterator.all response");
+ let allResponse = await iterator.all();
+ deepEqual(allResponse, sliceResponse, "iterator.all response has the expected data");
+
+ do_print("Check iterator response for 2 properties only");
+ sliceResponse = await iterator.slice(2, 2);
+ ok(sliceResponse && Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"),
+ "The response object has an ownProperties property");
+
+ ownProperties = sliceResponse.ownProperties;
+ names = Object.keys(ownProperties);
+ equal(names.length, 2, "The response has the expected number of properties");
+ equal(names[0], `property_2_key`);
+ equal(names[1], `property_3_key`);
+ equal(ownProperties[names[0]].value, `property_2_value`);
+ equal(ownProperties[names[1]].value, `property_3_value`);
+}
+
+async function check_enum_symbols(response) {
+ do_print("Check enumProperties response");
+ ok(response && Object.getOwnPropertyNames(response).includes("iterator"),
+ "The response object has an iterator property");
+
+ const {iterator} = response;
+ equal(iterator.count, 13, "iterator.count has the expected value");
+
+ do_print("Check iterator.slice response for all symbols");
+ let sliceResponse = await iterator.slice(0, iterator.count);
+ ok(sliceResponse && Object.getOwnPropertyNames(sliceResponse).includes("ownSymbols"),
+ "The response object has an ownSymbols property");
+
+ let {ownSymbols} = sliceResponse;
+ equal(ownSymbols.length, iterator.count,
+ "The response has the expected number of symbols");
+ for (let i = 0; i < 10; i++) {
+ const symbol = ownSymbols[i];
+ equal(symbol.name, `Symbol(symbol_${i})`);
+ equal(symbol.descriptor.value, `symbol_${i}_value`);
+ }
+ const firstUnnamedSymbol = ownSymbols[10];
+ equal(firstUnnamedSymbol.name, "Symbol()");
+ equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol");
+
+ const secondUnnamedSymbol = ownSymbols[11];
+ equal(secondUnnamedSymbol.name, "Symbol()");
+ equal(secondUnnamedSymbol.descriptor.value, "second unnamed symbol");
+
+ const iteratorSymbol = ownSymbols[12];
+ equal(iteratorSymbol.name, "Symbol(Symbol.iterator)");
+ equal(iteratorSymbol.descriptor.value.class, "Function");
+
+ do_print("Check iterator.all response");
+ let allResponse = await iterator.all();
+ deepEqual(allResponse, sliceResponse, "iterator.all response has the expected data");
+
+ do_print("Check iterator response for 2 symbols only");
+ sliceResponse = await iterator.slice(9, 2);
+ ok(sliceResponse && Object.getOwnPropertyNames(sliceResponse).includes("ownSymbols"),
+ "The response object has an ownSymbols property");
+
+ ownSymbols = sliceResponse.ownSymbols;
+ equal(ownSymbols.length, 2, "The response has the expected number of symbols");
+ equal(ownSymbols[0].name, "Symbol(symbol_9)");
+ equal(ownSymbols[0].descriptor.value, "symbol_9_value");
+ equal(ownSymbols[1].name, "Symbol()");
+ equal(ownSymbols[1].descriptor.value, "first unnamed symbol");
+}
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -168,16 +168,17 @@ reason = only ran on B2G
[test_objectgrips-10.js]
[test_objectgrips-11.js]
[test_objectgrips-12.js]
[test_objectgrips-13.js]
[test_objectgrips-14.js]
[test_objectgrips-15.js]
[test_objectgrips-16.js]
[test_objectgrips-17.js]
+[test_objectgrips-18.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/main.js
+++ b/devtools/shared/client/main.js
@@ -2655,16 +2655,40 @@ ObjectClient.prototype = {
iterator: new PropertyIteratorClient(this._client, response.iterator)
};
}
return response;
}
}),
/**
+ * Request a SymbolIteratorClient instance to enumerate symbols in an object.
+ *
+ * @param onResponse function Called with the request's response.
+ */
+ enumSymbols: DebuggerClient.requester({
+ type: "enumSymbols"
+ }, {
+ before: function (packet) {
+ if (this._grip.type !== "object") {
+ throw new Error("enumSymbols is only valid for objects grips.");
+ }
+ return packet;
+ },
+ after: function (response) {
+ if (response.iterator) {
+ return {
+ iterator: new SymbolIteratorClient(this._client, response.iterator)
+ };
+ }
+ return response;
+ }
+ }),
+
+ /**
* Request the property descriptor of the object's specified property.
*
* @param name string The name of the requested property.
* @param onResponse function Called with the request's response.
*/
getProperty: DebuggerClient.requester({
type: "property",
name: arg(0)
@@ -2832,16 +2856,71 @@ PropertyIteratorClient.prototype = {
* The function called when we receive the property values.
*/
all: DebuggerClient.requester({
type: "all"
}, {}),
};
/**
+ * A SymbolIteratorClient provides a way to access to symbols
+ * of an object efficiently, slice by slice.
+ *
+ * @param client DebuggerClient
+ * The debugger client parent.
+ * @param grip Object
+ * A SymbolIteratorActor grip returned by the protocol via
+ * TabActor.enumSymbols request.
+ */
+function SymbolIteratorClient(client, grip) {
+ this._grip = grip;
+ this._client = client;
+ this.request = this._client.request;
+}
+
+SymbolIteratorClient.prototype = {
+ get actor() {
+ return this._grip.actor;
+ },
+
+ /**
+ * Get the total number of symbols available in the iterator.
+ */
+ get count() {
+ return this._grip.count;
+ },
+
+ /**
+ * Get a set of following symbols.
+ *
+ * @param start Number
+ * The index of the first symbol to fetch.
+ * @param count Number
+ * The number of symbols to fetch.
+ * @param callback Function
+ * The function called when we receive the symbols.
+ */
+ slice: DebuggerClient.requester({
+ type: "slice",
+ start: arg(0),
+ count: arg(1)
+ }, {}),
+
+ /**
+ * Get all the symbols.
+ *
+ * @param callback Function
+ * The function called when we receive the symbols.
+ */
+ all: DebuggerClient.requester({
+ type: "all"
+ }, {}),
+};
+
+/**
* A ArrayBufferClient provides a way to access ArrayBuffer from the
* debugger server.
*
* @param client DebuggerClient
* The debugger client parent.
* @param grip Object
* A pause-lifetime ArrayBuffer grip returned by the protocol.
*/