summaryrefslogtreecommitdiffstats
path: root/devtools/shared/protocol/tests
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/shared/protocol/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/protocol/tests/xpcshell/head.js99
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js81
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_async.js193
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_children.js695
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_index.js52
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js52
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js27
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js308
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js322
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js99
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_types.js65
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js41
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js185
-rw-r--r--devtools/shared/protocol/tests/xpcshell/xpcshell.ini19
15 files changed, 2244 insertions, 0 deletions
diff --git a/devtools/shared/protocol/tests/xpcshell/.eslintrc.js b/devtools/shared/protocol/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/protocol/tests/xpcshell/head.js b/devtools/shared/protocol/tests/xpcshell/head.js
new file mode 100644
index 0000000000..dd055ddb42
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/head.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+
+function dumpn(msg) {
+ dump("DBG-TEST: " + msg + "\n");
+}
+
+function connectPipeTracing() {
+ return new TracingTransport(DevToolsServer.connectPipe());
+}
+
+/**
+ * Mock the `Transport` class in order to intercept all the packet
+ * getting in and out and then being able to assert them and dump them.
+ */
+function TracingTransport(childTransport) {
+ this.hooks = null;
+ this.child = childTransport;
+ this.child.hooks = this;
+
+ this.expectations = [];
+ this.packets = [];
+ this.checkIndex = 0;
+}
+
+TracingTransport.prototype = {
+ // Remove actor names
+ normalize(packet) {
+ return JSON.parse(
+ JSON.stringify(packet, (key, value) => {
+ if (key === "to" || key === "from" || key === "actor") {
+ return "<actorid>";
+ }
+ return value;
+ })
+ );
+ },
+ send(packet) {
+ this.packets.push({
+ type: "sent",
+ packet: this.normalize(packet),
+ });
+ return this.child.send(packet);
+ },
+ close() {
+ return this.child.close();
+ },
+ ready() {
+ return this.child.ready();
+ },
+ onPacket(packet) {
+ this.packets.push({
+ type: "received",
+ packet: this.normalize(packet),
+ });
+ this.hooks.onPacket(packet);
+ },
+ onTransportClosed() {
+ if (this.hooks.onTransportClosed) {
+ this.hooks.onTransportClosed();
+ }
+ },
+
+ expectSend(expected) {
+ const packet = this.packets[this.checkIndex++];
+ Assert.equal(packet.type, "sent");
+ deepEqual(packet.packet, this.normalize(expected));
+ },
+
+ expectReceive(expected) {
+ const packet = this.packets[this.checkIndex++];
+ Assert.equal(packet.type, "received");
+ deepEqual(packet.packet, this.normalize(expected));
+ },
+
+ // Write your tests, call dumpLog at the end, inspect the output,
+ // then sprinkle the calls through the right places in your test.
+ dumpLog() {
+ for (const entry of this.packets) {
+ if (entry.type === "sent") {
+ dumpn("trace.expectSend(" + entry.packet + ");");
+ } else {
+ dumpn("trace.expectReceive(" + entry.packet + ");");
+ }
+ }
+ },
+};
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js
new file mode 100644
index 0000000000..d66bf29a36
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Outstanding requests should be rejected when the connection aborts
+ * unexpectedly.
+ */
+
+var protocol = require("resource://devtools/shared/protocol.js");
+var { RetVal } = protocol;
+
+function simpleHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+}
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ },
+});
+
+var RootActor = protocol.ActorClassWithSpec(rootSpec, {
+ typeName: "root",
+ initialize(conn) {
+ protocol.Actor.prototype.initialize.call(this, conn);
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ this.sequence = 0;
+ },
+
+ sayHello: simpleHello,
+
+ simpleReturn() {
+ return this.sequence++;
+ },
+});
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function() {
+ DevToolsServer.createRootActor = RootActor;
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ await client.connect();
+
+ const rootFront = client.mainRoot;
+
+ const onSimpleReturn = rootFront.simpleReturn();
+ trace.close();
+
+ try {
+ await onSimpleReturn;
+ ok(false, "Connection was aborted, request shouldn't resolve");
+ } catch (e) {
+ const error = e.toString();
+ ok(true, "Connection was aborted, request rejected correctly");
+ ok(error.includes("Request stack:"), "Error includes request stack");
+ ok(error.includes("test_protocol_abort.js"), "Stack includes this test");
+ }
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_async.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_async.js
new file mode 100644
index 0000000000..d8760f15e2
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_async.js
@@ -0,0 +1,193 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Make sure we get replies in the same order that we sent their
+ * requests even when earlier requests take several event ticks to
+ * complete.
+ */
+
+const { waitForTick } = require("resource://devtools/shared/DevToolsUtils.js");
+const protocol = require("resource://devtools/shared/protocol.js");
+const { Arg, RetVal } = protocol;
+
+function simpleHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+}
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ promiseReturn: {
+ request: { toWait: Arg(0, "number") },
+ response: { value: RetVal("number") },
+ },
+ simpleThrow: {
+ response: { value: RetVal("number") },
+ },
+ promiseThrow: {
+ request: { toWait: Arg(0, "number") },
+ response: { value: RetVal("number") },
+ },
+ },
+});
+
+const RootActor = protocol.ActorClassWithSpec(rootSpec, {
+ initialize(conn) {
+ protocol.Actor.prototype.initialize.call(this, conn);
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ this.sequence = 0;
+ },
+
+ sayHello: simpleHello,
+
+ simpleReturn() {
+ return this.sequence++;
+ },
+
+ // Guarantee that this resolves after simpleReturn returns.
+ async promiseReturn(toWait) {
+ const sequence = this.sequence++;
+
+ // Wait until the number of requests specified by toWait have
+ // happened, to test queuing.
+ while (this.sequence - sequence < toWait) {
+ await waitForTick();
+ }
+
+ return sequence;
+ },
+
+ simpleThrow() {
+ throw new Error(this.sequence++);
+ },
+
+ // Guarantee that this resolves after simpleReturn returns.
+ promiseThrow(toWait) {
+ return this.promiseReturn(toWait).then(Promise.reject);
+ },
+});
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function() {
+ DevToolsServer.createRootActor = RootActor;
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ await client.connect();
+
+ const rootFront = client.mainRoot;
+
+ const calls = [];
+ let sequence = 0;
+
+ // Execute a call that won't finish processing until 2
+ // more calls have happened
+ calls.push(
+ rootFront.promiseReturn(2).then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 0);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ // Put a few requests into the backlog
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 1);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 2);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ calls.push(
+ rootFront.simpleThrow().then(
+ () => {
+ Assert.ok(false, "simpleThrow shouldn't succeed!");
+ },
+ error => {
+ // Check right return order
+ Assert.equal(sequence++, 3);
+ }
+ )
+ );
+
+ calls.push(
+ rootFront.promiseThrow(2).then(
+ () => {
+ Assert.ok(false, "promiseThrow shouldn't succeed!");
+ },
+ error => {
+ // Check right return order
+ Assert.equal(sequence++, 4);
+ Assert.ok(true, "simple throw should throw");
+ }
+ )
+ );
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 5);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ // Break up the backlog with a long request that waits
+ // for another simpleReturn before completing
+ calls.push(
+ rootFront.promiseReturn(1).then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 6);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 7);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ await Promise.all(calls);
+ await client.close();
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js
new file mode 100644
index 0000000000..4881c9b502
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js
@@ -0,0 +1,695 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Test simple requests using the protocol helpers.
+ */
+const protocol = require("resource://devtools/shared/protocol.js");
+const { types, Arg, RetVal } = protocol;
+
+function simpleHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+}
+
+// Predeclaring the actor type so that it can be used in the
+// implementation of the child actor.
+types.addActorType("childActor");
+types.addActorType("otherChildActor");
+types.addPolymorphicType("polytype", ["childActor", "otherChildActor"]);
+
+const childSpec = protocol.generateActorSpec({
+ typeName: "childActor",
+
+ events: {
+ event1: {
+ a: Arg(0),
+ b: Arg(1),
+ c: Arg(2),
+ },
+ event2: {
+ a: Arg(0),
+ b: Arg(1),
+ c: Arg(2),
+ },
+ "named-event": {
+ type: "namedEvent",
+ a: Arg(0),
+ b: Arg(1),
+ c: Arg(2),
+ },
+ "object-event": {
+ type: "objectEvent",
+ detail: Arg(0, "childActor#actorid"),
+ },
+ "array-object-event": {
+ type: "arrayObjectEvent",
+ detail: Arg(0, "array:childActor#actorid"),
+ },
+ },
+
+ methods: {
+ echo: {
+ request: { str: Arg(0) },
+ response: { str: RetVal("string") },
+ },
+ getDetail1: {
+ response: {
+ child: RetVal("childActor#actorid"),
+ },
+ },
+ getDetail2: {
+ response: {
+ child: RetVal("childActor#actorid"),
+ },
+ },
+ getIDDetail: {
+ response: {
+ idDetail: RetVal("childActor#actorid"),
+ },
+ },
+ getIntArray: {
+ request: { inputArray: Arg(0, "array:number") },
+ response: {
+ intArray: RetVal("array:number"),
+ },
+ },
+ getSibling: {
+ request: { id: Arg(0) },
+ response: { sibling: RetVal("childActor") },
+ },
+ emitEvents: {
+ response: { value: RetVal("string") },
+ },
+ release: {
+ release: true,
+ },
+ },
+});
+
+var ChildActor = protocol.ActorClassWithSpec(childSpec, {
+ // Actors returned by this actor should be owned by the root actor.
+ marshallPool() {
+ return this.getParent();
+ },
+
+ toString() {
+ return "[ChildActor " + this.childID + "]";
+ },
+
+ initialize(conn, id) {
+ protocol.Actor.prototype.initialize.call(this, conn);
+ this.childID = id;
+ },
+
+ destroy() {
+ protocol.Actor.prototype.destroy.call(this);
+ this.destroyed = true;
+ },
+
+ form() {
+ return {
+ actor: this.actorID,
+ childID: this.childID,
+ };
+ },
+
+ echo(str) {
+ return str;
+ },
+
+ getDetail1() {
+ return this;
+ },
+
+ getDetail2() {
+ return this;
+ },
+
+ getIDDetail() {
+ return this;
+ },
+
+ getIntArray(inputArray) {
+ // Test that protocol.js converts an iterator to an array.
+ const f = function*() {
+ for (const i of inputArray) {
+ yield 2 * i;
+ }
+ };
+ return f();
+ },
+
+ getSibling(id) {
+ return this.getParent().getChild(id);
+ },
+
+ emitEvents() {
+ this.emit("event1", 1, 2, 3);
+ this.emit("event2", 4, 5, 6);
+ this.emit("named-event", 1, 2, 3);
+ this.emit("object-event", this);
+ this.emit("array-object-event", [this]);
+ return "correct response";
+ },
+
+ release() {},
+});
+
+class ChildFront extends protocol.FrontClassWithSpec(childSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._parentFront = parentFront;
+
+ this.before("event1", this.onEvent1.bind(this));
+ this.before("event2", this.onEvent2a.bind(this));
+ this.on("event2", this.onEvent2b.bind(this));
+ }
+
+ destroy() {
+ this.destroyed = true;
+ // Call parent's destroy, which may be re-entrant and recall this function
+ this._parentFront.destroy();
+ super.destroy();
+ }
+
+ marshallPool() {
+ return this.getParent();
+ }
+
+ toString() {
+ return "[child front " + this.childID + "]";
+ }
+
+ form(form) {
+ this.childID = form.childID;
+ }
+
+ onEvent1(a, b, c) {
+ this.event1arg3 = c;
+ }
+
+ onEvent2a(a, b, c) {
+ return Promise.resolve().then(() => {
+ this.event2arg3 = c;
+ });
+ }
+
+ onEvent2b(a, b, c) {
+ this.event2arg2 = b;
+ }
+}
+protocol.registerFront(ChildFront);
+
+const otherChildSpec = protocol.generateActorSpec({
+ typeName: "otherChildActor",
+ methods: {
+ getOtherChild: {
+ request: {},
+ response: { sibling: RetVal("otherChildActor") },
+ },
+ },
+ events: {},
+});
+const OtherChildActor = protocol.ActorClassWithSpec(otherChildSpec, {
+ getOtherChild() {
+ return new OtherChildActor(this.conn);
+ },
+});
+class OtherChildFront extends protocol.FrontClassWithSpec(otherChildSpec) {}
+protocol.registerFront(OtherChildFront);
+
+types.addDictType("manyChildrenDict", {
+ child5: "childActor",
+ more: "array:childActor",
+});
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ getChild: {
+ request: { str: Arg(0) },
+ response: { actor: RetVal("childActor") },
+ },
+ getOtherChild: {
+ request: {},
+ response: { sibling: RetVal("otherChildActor") },
+ },
+ getChildren: {
+ request: { ids: Arg(0, "array:string") },
+ response: { children: RetVal("array:childActor") },
+ },
+ getChildren2: {
+ request: { ids: Arg(0, "array:childActor") },
+ response: { children: RetVal("array:childActor") },
+ },
+ getManyChildren: {
+ response: RetVal("manyChildrenDict"),
+ },
+ getPolymorphism: {
+ request: { id: Arg(0, "number") },
+ response: { child: RetVal("polytype") },
+ },
+ requestPolymorphism: {
+ request: {
+ id: Arg(0, "number"),
+ actor: Arg(1, "polytype"),
+ },
+ response: { child: RetVal("polytype") },
+ },
+ },
+});
+
+let rootActor = null;
+const RootActor = protocol.ActorClassWithSpec(rootSpec, {
+ toString() {
+ return "[root actor]";
+ },
+
+ initialize(conn) {
+ rootActor = this;
+ this.actorID = "root";
+ this._children = {};
+ protocol.Actor.prototype.initialize.call(this, conn);
+ },
+
+ sayHello: simpleHello,
+
+ getChild(id) {
+ if (id in this._children) {
+ return this._children[id];
+ }
+ const child = new ChildActor(this.conn, id);
+ this._children[id] = child;
+ return child;
+ },
+
+ // Other child actor won't all be own by the root actor
+ // and can have their own children
+ getOtherChild() {
+ return new OtherChildActor(this.conn);
+ },
+
+ getChildren(ids) {
+ return ids.map(id => this.getChild(id));
+ },
+
+ getChildren2(ids) {
+ const f = function*() {
+ for (const c of ids) {
+ yield c;
+ }
+ };
+ return f();
+ },
+
+ getManyChildren() {
+ return {
+ // note that this isn't in the specialization array.
+ foo: "bar",
+ child5: this.getChild("child5"),
+ more: [this.getChild("child6"), this.getChild("child7")],
+ };
+ },
+
+ getPolymorphism(id) {
+ if (id == 0) {
+ return new ChildActor(this.conn, id);
+ } else if (id == 1) {
+ return new OtherChildActor(this.conn);
+ }
+ throw new Error("Unexpected id");
+ },
+
+ requestPolymorphism(id, actor) {
+ if (id == 0 && actor instanceof ChildActor) {
+ return actor;
+ } else if (id == 1 && actor instanceof OtherChildActor) {
+ return actor;
+ }
+ throw new Error("Unexpected id or actor");
+ },
+});
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.actorID = "root";
+ // Root actor owns itself.
+ this.manage(this);
+ }
+
+ toString() {
+ return "[root front]";
+ }
+}
+
+let rootFront, childFront;
+function expectRootChildren(size) {
+ Assert.equal(rootActor._poolMap.size, size);
+ Assert.equal(rootFront._poolMap.size, size + 1);
+ if (childFront) {
+ Assert.equal(childFront._poolMap.size, 0);
+ }
+}
+protocol.registerFront(RootFront);
+
+function childrenOfType(pool, type) {
+ const children = [...rootFront.poolChildren()];
+ return children.filter(child => child instanceof type);
+}
+
+add_task(async function() {
+ DevToolsServer.createRootActor = conn => {
+ return RootActor(conn);
+ };
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ const [applicationType] = await client.connect();
+ trace.expectReceive({
+ from: "<actorid>",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ });
+ Assert.equal(applicationType, "xpcshell-tests");
+
+ rootFront = client.mainRoot;
+
+ await testSimpleChildren(trace);
+ await testDetail(trace);
+ await testSibling(trace);
+ await testEvents(trace);
+ await testManyChildren(trace);
+ await testGenerator(trace);
+ await testPolymorphism(trace);
+ await testUnmanageChildren(trace);
+ // Execute that assertion very last as it destroy the root front and actor
+ await testDestroy(trace);
+
+ await client.close();
+});
+
+async function testSimpleChildren(trace) {
+ childFront = await rootFront.getChild("child1");
+ trace.expectSend({ type: "getChild", str: "child1", to: "<actorid>" });
+ trace.expectReceive({ actor: "<actorid>", from: "<actorid>" });
+
+ Assert.ok(childFront instanceof ChildFront);
+ Assert.equal(childFront.childID, "child1");
+ expectRootChildren(1);
+
+ // Request the child again, make sure the same is returned.
+ let ret = await rootFront.getChild("child1");
+ trace.expectSend({ type: "getChild", str: "child1", to: "<actorid>" });
+ trace.expectReceive({ actor: "<actorid>", from: "<actorid>" });
+
+ expectRootChildren(1);
+ Assert.ok(ret === childFront);
+
+ ret = await childFront.echo("hello");
+ trace.expectSend({ type: "echo", str: "hello", to: "<actorid>" });
+ trace.expectReceive({ str: "hello", from: "<actorid>" });
+
+ Assert.equal(ret, "hello");
+}
+
+async function testDetail(trace) {
+ let ret = await childFront.getDetail1();
+ trace.expectSend({ type: "getDetail1", to: "<actorid>" });
+ trace.expectReceive({ child: childFront.actorID, from: "<actorid>" });
+ Assert.ok(ret === childFront);
+
+ ret = await childFront.getDetail2();
+ trace.expectSend({ type: "getDetail2", to: "<actorid>" });
+ trace.expectReceive({ child: childFront.actorID, from: "<actorid>" });
+ Assert.ok(ret === childFront);
+
+ ret = await childFront.getIDDetail();
+ trace.expectSend({ type: "getIDDetail", to: "<actorid>" });
+ trace.expectReceive({
+ idDetail: childFront.actorID,
+ from: "<actorid>",
+ });
+ Assert.ok(ret === childFront);
+}
+
+async function testSibling(trace) {
+ await childFront.getSibling("siblingID");
+ trace.expectSend({
+ type: "getSibling",
+ id: "siblingID",
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ sibling: { actor: "<actorid>", childID: "siblingID" },
+ from: "<actorid>",
+ });
+
+ expectRootChildren(2);
+}
+
+async function testEvents(trace) {
+ const ret = await rootFront.getChildren(["child1", "child2"]);
+ trace.expectSend({
+ type: "getChildren",
+ ids: ["child1", "child2"],
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ children: [
+ { actor: "<actorid>", childID: "child1" },
+ { actor: "<actorid>", childID: "child2" },
+ ],
+ from: "<actorid>",
+ });
+
+ expectRootChildren(3);
+ Assert.ok(ret[0] === childFront);
+ Assert.ok(ret[1] !== childFront);
+ Assert.ok(ret[1] instanceof ChildFront);
+
+ // On both children, listen to events. We're only
+ // going to trigger events on the first child, so an event
+ // triggered on the second should cause immediate failures.
+
+ const set = new Set([
+ "event1",
+ "event2",
+ "named-event",
+ "object-event",
+ "array-object-event",
+ ]);
+
+ childFront.on("event1", (a, b, c) => {
+ Assert.equal(a, 1);
+ Assert.equal(b, 2);
+ Assert.equal(c, 3);
+ // Verify that the pre-event handler was called.
+ Assert.equal(childFront.event1arg3, 3);
+ set.delete("event1");
+ });
+ childFront.on("event2", (a, b, c) => {
+ Assert.equal(a, 4);
+ Assert.equal(b, 5);
+ Assert.equal(c, 6);
+ // Verify that the async pre-event handler was called,
+ // setting the property before this handler was called.
+ Assert.equal(childFront.event2arg3, 6);
+ // And check that the sync preEvent with the same name is also
+ // executed
+ Assert.equal(childFront.event2arg2, 5);
+ set.delete("event2");
+ });
+ childFront.on("named-event", (a, b, c) => {
+ Assert.equal(a, 1);
+ Assert.equal(b, 2);
+ Assert.equal(c, 3);
+ set.delete("named-event");
+ });
+ childFront.on("object-event", obj => {
+ Assert.ok(obj === childFront);
+ set.delete("object-event");
+ });
+ childFront.on("array-object-event", array => {
+ Assert.ok(array[0] === childFront);
+ set.delete("array-object-event");
+ });
+
+ const fail = function() {
+ do_throw("Unexpected event");
+ };
+ ret[1].on("event1", fail);
+ ret[1].on("event2", fail);
+ ret[1].on("named-event", fail);
+ ret[1].on("object-event", fail);
+ ret[1].on("array-object-event", fail);
+
+ await childFront.emitEvents();
+ trace.expectSend({ type: "emitEvents", to: "<actorid>" });
+ trace.expectReceive({
+ type: "event1",
+ a: 1,
+ b: 2,
+ c: 3,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "event2",
+ a: 4,
+ b: 5,
+ c: 6,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "namedEvent",
+ a: 1,
+ b: 2,
+ c: 3,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "objectEvent",
+ detail: childFront.actorID,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "arrayObjectEvent",
+ detail: [childFront.actorID],
+ from: "<actorid>",
+ });
+ trace.expectReceive({ value: "correct response", from: "<actorid>" });
+
+ Assert.equal(set.size, 0);
+}
+
+async function testManyChildren(trace) {
+ const ret = await rootFront.getManyChildren();
+ trace.expectSend({ type: "getManyChildren", to: "<actorid>" });
+ trace.expectReceive({
+ foo: "bar",
+ child5: { actor: "<actorid>", childID: "child5" },
+ more: [
+ { actor: "<actorid>", childID: "child6" },
+ { actor: "<actorid>", childID: "child7" },
+ ],
+ from: "<actorid>",
+ });
+
+ // Check all the crazy stuff we did in getManyChildren
+ Assert.equal(ret.foo, "bar");
+ Assert.equal(ret.child5.childID, "child5");
+ Assert.equal(ret.more[0].childID, "child6");
+ Assert.equal(ret.more[1].childID, "child7");
+}
+
+async function testGenerator(trace) {
+ // Test accepting a generator.
+ const f = function*() {
+ for (const i of [1, 2, 3, 4, 5]) {
+ yield i;
+ }
+ };
+ let ret = await childFront.getIntArray(f());
+ Assert.equal(ret.length, 5);
+ const expected = [2, 4, 6, 8, 10];
+ for (let i = 0; i < 5; ++i) {
+ Assert.equal(ret[i], expected[i]);
+ }
+
+ const ids = await rootFront.getChildren(["child1", "child2"]);
+ const f2 = function*() {
+ for (const id of ids) {
+ yield id;
+ }
+ };
+ ret = await rootFront.getChildren2(f2());
+ Assert.equal(ret.length, 2);
+ Assert.ok(ret[0] === childFront);
+ Assert.ok(ret[1] !== childFront);
+ Assert.ok(ret[1] instanceof ChildFront);
+}
+
+async function testPolymorphism(trace) {
+ // Check polymorphic types returned by an actor
+ const firstChild = await rootFront.getPolymorphism(0);
+ Assert.ok(firstChild instanceof ChildFront);
+
+ // Check polymorphic types passed to a front
+ const sameFirstChild = await rootFront.requestPolymorphism(0, firstChild);
+ Assert.ok(sameFirstChild instanceof ChildFront);
+ Assert.equal(sameFirstChild, firstChild);
+
+ // Same with the second possible type
+ const secondChild = await rootFront.getPolymorphism(1);
+ Assert.ok(secondChild instanceof OtherChildFront);
+
+ const sameSecondChild = await rootFront.requestPolymorphism(1, secondChild);
+ Assert.ok(sameSecondChild instanceof OtherChildFront);
+ Assert.equal(sameSecondChild, secondChild);
+
+ // Check that any other type is rejected
+ Assert.throws(() => {
+ rootFront.requestPolymorphism(0, null);
+ }, /Was expecting one of these actors 'childActor,otherChildActor' but instead got an empty value/);
+ Assert.throws(() => {
+ rootFront.requestPolymorphism(0, 42);
+ }, /Was expecting one of these actors 'childActor,otherChildActor' but instead got value: '42'/);
+ Assert.throws(() => {
+ rootFront.requestPolymorphism(0, rootFront);
+ }, /Was expecting one of these actors 'childActor,otherChildActor' but instead got an actor of type: 'root'/);
+}
+
+async function testUnmanageChildren(trace) {
+ // There is already one front of type OtherChildFront
+ Assert.equal(childrenOfType(rootFront, OtherChildFront).length, 1);
+
+ // Create another front of type OtherChildFront
+ const front = await rootFront.getPolymorphism(1);
+ Assert.ok(front instanceof OtherChildFront);
+ Assert.equal(childrenOfType(rootFront, OtherChildFront).length, 2);
+
+ // Remove all fronts of type OtherChildFront
+ rootFront.unmanageChildren(OtherChildFront);
+ Assert.ok(
+ !front.isDestroyed(),
+ "Unmanaged front is not considered as destroyed"
+ );
+ Assert.equal(childrenOfType(rootFront, OtherChildFront).length, 0);
+}
+
+async function testDestroy(trace) {
+ const front = await rootFront.getOtherChild();
+ const otherChildFront = await front.getOtherChild();
+ Assert.equal(
+ otherChildFront.getParent(),
+ front,
+ "the child is a children of first front"
+ );
+
+ front.destroy();
+ Assert.ok(front.isDestroyed(), "sibling is correctly reported as destroyed");
+ Assert.ok(!front.getParent(), "sibling has no more parent declared");
+ Assert.ok(otherChildFront.isDestroyed(), "the child is also destroyed");
+ Assert.ok(
+ !otherChildFront.getParent(),
+ "the child also has no more parent declared"
+ );
+ Assert.ok(
+ !otherChildFront.parentPool,
+ "the child also has its parentPool attribute nullified"
+ );
+
+ // Verify that re-entrant Front.destroy doesn't throw, nor loop
+ // Execute that very last as it will destroy the root actor and front
+ const sibling = await childFront.getSibling("siblingID");
+ sibling.destroy();
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_index.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_index.js
new file mode 100644
index 0000000000..2e38547355
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_index.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const { lazyLoadFront } = require("resource://devtools/shared/specs/index.js");
+const Types = require("resource://devtools/shared/specs/index.js")
+ .__TypesForTests;
+const { getType } = require("resource://devtools/shared/protocol.js").types;
+
+function run_test() {
+ test_index_is_alphabetically_sorted();
+ test_specs();
+ test_fronts();
+}
+
+// Check alphabetic order of specs defined in devtools/shared/specs/index.js,
+// in order to ease its maintenance and readability.
+function test_index_is_alphabetically_sorted() {
+ let lastSpec = "";
+ for (const type of Types) {
+ const spec = type.spec;
+ if (lastSpec && spec < lastSpec) {
+ ok(false, `Spec definition for "${spec}" should be before "${lastSpec}"`);
+ }
+ lastSpec = spec;
+ }
+ ok(true, "Specs index is alphabetically sorted");
+}
+
+function test_specs() {
+ for (const type of Types) {
+ for (const typeName of type.types) {
+ ok(!!getType(typeName), `${typeName} spec is defined`);
+ }
+ }
+ ok(true, "Specs are all accessible");
+}
+
+function test_fronts() {
+ for (const item of Types) {
+ if (!item.front) {
+ continue;
+ }
+ for (const typeName of item.types) {
+ lazyLoadFront(typeName);
+ const type = getType(typeName);
+ ok(!!type, `Front for ${typeName} has a spec`);
+ ok(type.frontClass, `${typeName} has a front correctly defined`);
+ }
+ }
+ ok(true, "Front are all accessible");
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js
new file mode 100644
index 0000000000..a785e5c3a7
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const protocol = require("resource://devtools/shared/protocol.js");
+const { RetVal } = protocol;
+
+// Test invalid response specs throw when generating the Actor specification.
+
+// Test top level array response
+add_task(async function() {
+ Assert.throws(() => {
+ protocol.generateActorSpec({
+ typeName: "invalidArrayResponse",
+ methods: {
+ invalidMethod: {
+ response: RetVal("array:string"),
+ },
+ },
+ });
+ }, /Arrays should be wrapped in objects/);
+
+ protocol.generateActorSpec({
+ typeName: "validArrayResponse",
+ methods: {
+ validMethod: {
+ response: {
+ someArray: RetVal("array:string"),
+ },
+ },
+ },
+ });
+ ok(true, "Arrays wrapped in object are valid response packets");
+});
+
+// Test response with several placeholders
+add_task(async function() {
+ Assert.throws(() => {
+ protocol.generateActorSpec({
+ typeName: "tooManyPlaceholdersResponse",
+ methods: {
+ invalidMethod: {
+ response: {
+ prop1: RetVal("json"),
+ prop2: RetVal("json"),
+ },
+ },
+ },
+ });
+ }, /More than one RetVal specified in response/);
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js
new file mode 100644
index 0000000000..7a449de555
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+const { Front } = require("resource://devtools/shared/protocol/Front.js");
+
+add_task(async function() {
+ // Front constructor expect to be provided a client object
+ const client = {};
+ const front = new Front(client);
+ ok(
+ !front.isDestroyed(),
+ "Blank front with no actor ID is not considered as destroyed"
+ );
+ front.destroy();
+ ok(front.isDestroyed(), "Front is destroyed");
+
+ const actor = new Actor();
+ ok(
+ !actor.isDestroyed(),
+ "Blank actor with no actor ID is not considered as destroyed"
+ );
+ actor.destroy();
+ ok(actor.isDestroyed(), "Actor is destroyed");
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js
new file mode 100644
index 0000000000..e62fb061f5
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js
@@ -0,0 +1,308 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Test simple requests using the protocol helpers.
+ */
+var protocol = require("resource://devtools/shared/protocol.js");
+var { RetVal, Arg } = protocol;
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+var {
+ LongStringActor,
+} = require("resource://devtools/server/actors/string.js");
+
+// The test implicitly relies on this.
+require("resource://devtools/client/fronts/string.js");
+
+function simpleHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+}
+
+DevToolsServer.LONG_STRING_LENGTH = DevToolsServer.LONG_STRING_INITIAL_LENGTH = DevToolsServer.LONG_STRING_READ_LENGTH = 5;
+
+var SHORT_STR = "abc";
+var LONG_STR = "abcdefghijklmnop";
+
+var rootActor = null;
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ events: {
+ "string-event": {
+ str: Arg(0, "longstring"),
+ },
+ },
+
+ methods: {
+ shortString: {
+ response: { value: RetVal("longstring") },
+ },
+ longString: {
+ response: { value: RetVal("longstring") },
+ },
+ emitShortString: {
+ oneway: true,
+ },
+ emitLongString: {
+ oneway: true,
+ },
+ },
+});
+
+var RootActor = protocol.ActorClassWithSpec(rootSpec, {
+ initialize(conn) {
+ rootActor = this;
+ protocol.Actor.prototype.initialize.call(this, conn);
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ },
+
+ sayHello: simpleHello,
+
+ shortString() {
+ return new LongStringActor(this.conn, SHORT_STR);
+ },
+
+ longString() {
+ return new LongStringActor(this.conn, LONG_STR);
+ },
+
+ emitShortString() {
+ EventEmitter.emit(
+ this,
+ "string-event",
+ new LongStringActor(this.conn, SHORT_STR)
+ );
+ },
+
+ emitLongString() {
+ EventEmitter.emit(
+ this,
+ "string-event",
+ new LongStringActor(this.conn, LONG_STR)
+ );
+ },
+});
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+function run_test() {
+ DevToolsServer.createRootActor = conn => {
+ return RootActor(conn);
+ };
+
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ let rootFront;
+
+ let strfront = null;
+
+ const expectRootChildren = function(size) {
+ Assert.equal(rootActor.__poolMap.size, size + 1);
+ Assert.equal(rootFront.__poolMap.size, size + 1);
+ };
+
+ client.connect().then(([applicationType, traits]) => {
+ rootFront = client.mainRoot;
+
+ // Root actor has no children yet.
+ expectRootChildren(0);
+
+ trace.expectReceive({
+ from: "<actorid>",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ });
+ Assert.equal(applicationType, "xpcshell-tests");
+ rootFront
+ .shortString()
+ .then(ret => {
+ trace.expectSend({ type: "shortString", to: "<actorid>" });
+ trace.expectReceive({ value: "abc", from: "<actorid>" });
+
+ // Should only own the one reference (itself) at this point.
+ expectRootChildren(0);
+ strfront = ret;
+ })
+ .then(() => {
+ return strfront.string();
+ })
+ .then(ret => {
+ Assert.equal(ret, SHORT_STR);
+ })
+ .then(() => {
+ return rootFront.longString();
+ })
+ .then(ret => {
+ trace.expectSend({ type: "longString", to: "<actorid>" });
+ trace.expectReceive({
+ value: {
+ type: "longString",
+ actor: "<actorid>",
+ length: 16,
+ initial: "abcde",
+ },
+ from: "<actorid>",
+ });
+
+ strfront = ret;
+ // Should own a reference to itself and an extra string now.
+ expectRootChildren(1);
+ })
+ .then(() => {
+ return strfront.string();
+ })
+ .then(ret => {
+ trace.expectSend({
+ type: "substring",
+ start: 5,
+ end: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "fghij", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 10,
+ end: 15,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "klmno", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 15,
+ end: 20,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "p", from: "<actorid>" });
+
+ Assert.equal(ret, LONG_STR);
+ })
+ .then(() => {
+ return strfront.release();
+ })
+ .then(() => {
+ trace.expectSend({ type: "release", to: "<actorid>" });
+ trace.expectReceive({ from: "<actorid>" });
+
+ // That reference should be removed now.
+ expectRootChildren(0);
+ })
+ .then(() => {
+ return new Promise(resolve => {
+ rootFront.once("string-event", str => {
+ trace.expectSend({ type: "emitShortString", to: "<actorid>" });
+ trace.expectReceive({
+ type: "string-event",
+ str: "abc",
+ from: "<actorid>",
+ });
+
+ Assert.ok(!!str);
+ strfront = str;
+ // Shouldn't generate any new references
+ expectRootChildren(0);
+ // will generate no packets.
+ strfront.string().then(value => {
+ resolve(value);
+ });
+ });
+ rootFront.emitShortString();
+ });
+ })
+ .then(value => {
+ Assert.equal(value, SHORT_STR);
+ })
+ .then(() => {
+ // Will generate no packets
+ return strfront.release();
+ })
+ .then(() => {
+ return new Promise(resolve => {
+ rootFront.once("string-event", str => {
+ trace.expectSend({ type: "emitLongString", to: "<actorid>" });
+ trace.expectReceive({
+ type: "string-event",
+ str: {
+ type: "longString",
+ actor: "<actorid>",
+ length: 16,
+ initial: "abcde",
+ },
+ from: "<actorid>",
+ });
+
+ Assert.ok(!!str);
+ // Should generate one new reference
+ expectRootChildren(1);
+ strfront = str;
+ strfront.string().then(value => {
+ trace.expectSend({
+ type: "substring",
+ start: 5,
+ end: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "fghij", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 10,
+ end: 15,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "klmno", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 15,
+ end: 20,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "p", from: "<actorid>" });
+
+ resolve(value);
+ });
+ });
+ rootFront.emitLongString();
+ });
+ })
+ .then(value => {
+ Assert.equal(value, LONG_STR);
+ })
+ .then(() => {
+ return strfront.release();
+ })
+ .then(() => {
+ trace.expectSend({ type: "release", to: "<actorid>" });
+ trace.expectReceive({ from: "<actorid>" });
+ expectRootChildren(0);
+ })
+ .then(() => {
+ client.close().then(() => {
+ do_test_finished();
+ });
+ })
+ .catch(err => {
+ do_report_unexpected_exception(err, "Failure executing test");
+ });
+ });
+ do_test_pending();
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js
new file mode 100644
index 0000000000..ee8435a833
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js
@@ -0,0 +1,322 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test simple requests using the protocol helpers.
+ */
+
+var protocol = require("resource://devtools/shared/protocol.js");
+var { Arg, Option, RetVal } = protocol;
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+function simpleHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+}
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ events: {
+ oneway: { a: Arg(0) },
+ falsyOptions: {
+ zero: Option(0),
+ farce: Option(0),
+ },
+ },
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ promiseReturn: {
+ response: { value: RetVal("number") },
+ },
+ simpleArgs: {
+ request: {
+ firstArg: Arg(0),
+ secondArg: Arg(1),
+ },
+ response: RetVal(),
+ },
+ optionArgs: {
+ request: {
+ option1: Option(0),
+ option2: Option(0),
+ },
+ response: RetVal(),
+ },
+ optionalArgs: {
+ request: {
+ a: Arg(0),
+ b: Arg(1, "nullable:number"),
+ },
+ response: {
+ value: RetVal("number"),
+ },
+ },
+ arrayArgs: {
+ request: {
+ a: Arg(0, "array:number"),
+ },
+ response: {
+ arrayReturn: RetVal("array:number"),
+ },
+ },
+ nestedArrayArgs: {
+ request: { a: Arg(0, "array:array:number") },
+ response: { value: RetVal("array:array:number") },
+ },
+ renamedEcho: {
+ request: {
+ type: "echo",
+ a: Arg(0),
+ },
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ testOneWay: {
+ request: { a: Arg(0) },
+ oneway: true,
+ },
+ emitFalsyOptions: {
+ oneway: true,
+ },
+ },
+});
+
+var RootActor = protocol.ActorClassWithSpec(rootSpec, {
+ initialize(conn) {
+ protocol.Actor.prototype.initialize.call(this, conn);
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ },
+
+ sayHello: simpleHello,
+
+ simpleReturn() {
+ return 1;
+ },
+
+ promiseReturn() {
+ return Promise.resolve(1);
+ },
+
+ simpleArgs(a, b) {
+ return { firstResponse: a + 1, secondResponse: b + 1 };
+ },
+
+ optionArgs(options) {
+ return { option1: options.option1, option2: options.option2 };
+ },
+
+ optionalArgs(a, b = 200) {
+ return b;
+ },
+
+ arrayArgs(a) {
+ return a;
+ },
+
+ nestedArrayArgs(a) {
+ return a;
+ },
+
+ /**
+ * Test that the 'type' part of the request packet works
+ * correctly when the type isn't the same as the method name
+ */
+ renamedEcho(a) {
+ if (this.conn.currentPacket.type != "echo") {
+ return "goodbye";
+ }
+ return a;
+ },
+
+ testOneWay(a) {
+ // Emit to show that we got this message, because there won't be a response.
+ EventEmitter.emit(this, "oneway", a);
+ },
+
+ emitFalsyOptions() {
+ EventEmitter.emit(this, "falsyOptions", { zero: 0, farce: false });
+ },
+});
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function() {
+ DevToolsServer.createRootActor = conn => {
+ return RootActor(conn);
+ };
+ DevToolsServer.init();
+
+ Assert.throws(() => {
+ const badActor = protocol.ActorClassWithSpec({}, {});
+ void badActor;
+ }, /Actor specification must have a typeName member/);
+
+ protocol.types.getType("array:array:array:number");
+ protocol.types.getType("array:array:array:number");
+
+ Assert.throws(
+ () => protocol.types.getType("unknown"),
+ /Unknown type:/,
+ "Should throw for unknown type"
+ );
+ Assert.throws(
+ () => protocol.types.getType("array:unknown"),
+ /Unknown type:/,
+ "Should throw for unknown type"
+ );
+ Assert.throws(
+ () => protocol.types.getType("unknown:number"),
+ /Unknown collection type:/,
+ "Should throw for unknown collection type"
+ );
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+
+ const [applicationType] = await client.connect();
+ trace.expectReceive({
+ from: "<actorid>",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ });
+ Assert.equal(applicationType, "xpcshell-tests");
+
+ const rootFront = client.mainRoot;
+
+ let ret = await rootFront.simpleReturn();
+ trace.expectSend({ type: "simpleReturn", to: "<actorid>" });
+ trace.expectReceive({ value: 1, from: "<actorid>" });
+ Assert.equal(ret, 1);
+
+ ret = await rootFront.promiseReturn();
+ trace.expectSend({ type: "promiseReturn", to: "<actorid>" });
+ trace.expectReceive({ value: 1, from: "<actorid>" });
+ Assert.equal(ret, 1);
+
+ Assert.throws(
+ () => rootFront.simpleArgs(5),
+ /undefined passed where a value is required/,
+ "Should throw if simpleArgs is missing an argument."
+ );
+
+ ret = await rootFront.simpleArgs(5, 10);
+ trace.expectSend({
+ type: "simpleArgs",
+ firstArg: 5,
+ secondArg: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ firstResponse: 6,
+ secondResponse: 11,
+ from: "<actorid>",
+ });
+ Assert.equal(ret.firstResponse, 6);
+ Assert.equal(ret.secondResponse, 11);
+
+ ret = await rootFront.optionArgs({
+ option1: 5,
+ option2: 10,
+ });
+ trace.expectSend({
+ type: "optionArgs",
+ option1: 5,
+ option2: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ option1: 5, option2: 10, from: "<actorid>" });
+ Assert.equal(ret.option1, 5);
+ Assert.equal(ret.option2, 10);
+
+ ret = await rootFront.optionArgs({});
+ trace.expectSend({ type: "optionArgs", to: "<actorid>" });
+ trace.expectReceive({ from: "<actorid>" });
+ Assert.ok(typeof ret.option1 === "undefined");
+ Assert.ok(typeof ret.option2 === "undefined");
+
+ // Explicitly call an optional argument...
+ ret = await rootFront.optionalArgs(5, 10);
+ trace.expectSend({
+ type: "optionalArgs",
+ a: 5,
+ b: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ value: 10, from: "<actorid>" });
+ Assert.equal(ret, 10);
+
+ // Now don't pass the optional argument, expect the default.
+ ret = await rootFront.optionalArgs(5);
+ trace.expectSend({ type: "optionalArgs", a: 5, to: "<actorid>" });
+ trace.expectReceive({ value: 200, from: "<actorid>" });
+ Assert.equal(ret, 200);
+
+ ret = await rootFront.arrayArgs([0, 1, 2, 3, 4, 5]);
+ trace.expectSend({
+ type: "arrayArgs",
+ a: [0, 1, 2, 3, 4, 5],
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ arrayReturn: [0, 1, 2, 3, 4, 5],
+ from: "<actorid>",
+ });
+ Assert.equal(ret[0], 0);
+ Assert.equal(ret[5], 5);
+
+ ret = await rootFront.arrayArgs([[5]]);
+ trace.expectSend({ type: "arrayArgs", a: [[5]], to: "<actorid>" });
+ trace.expectReceive({ arrayReturn: [[5]], from: "<actorid>" });
+ Assert.equal(ret[0][0], 5);
+
+ const str = await rootFront.renamedEcho("hello");
+ trace.expectSend({ type: "echo", a: "hello", to: "<actorid>" });
+ trace.expectReceive({ value: "hello", from: "<actorid>" });
+ Assert.equal(str, "hello");
+
+ const onOneWay = rootFront.once("oneway");
+ Assert.ok(typeof rootFront.testOneWay("hello") === "undefined");
+ const response = await onOneWay;
+ trace.expectSend({ type: "testOneWay", a: "hello", to: "<actorid>" });
+ trace.expectReceive({
+ type: "oneway",
+ a: "hello",
+ from: "<actorid>",
+ });
+ Assert.equal(response, "hello");
+
+ const onFalsyOptions = rootFront.once("falsyOptions");
+ rootFront.emitFalsyOptions();
+ const res = await onFalsyOptions;
+ trace.expectSend({ type: "emitFalsyOptions", to: "<actorid>" });
+ trace.expectReceive({
+ type: "falsyOptions",
+ farce: false,
+ zero: 0,
+ from: "<actorid>",
+ });
+
+ Assert.ok(res.zero === 0);
+ Assert.ok(res.farce === false);
+
+ await client.close();
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js
new file mode 100644
index 0000000000..63feed5798
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Client request stacks should span the entire process from before making the
+ * request to handling the reply from the server. The server frames are not
+ * included, nor can they be in most cases, since the server can be a remote
+ * device.
+ */
+
+var protocol = require("resource://devtools/shared/protocol.js");
+var { RetVal } = protocol;
+
+function simpleHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+}
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ },
+});
+
+var RootActor = protocol.ActorClassWithSpec(rootSpec, {
+ initialize(conn) {
+ protocol.Actor.prototype.initialize.call(this, conn);
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ this.sequence = 0;
+ },
+
+ sayHello: simpleHello,
+
+ simpleReturn() {
+ return this.sequence++;
+ },
+});
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+function run_test() {
+ DevToolsServer.createRootActor = RootActor;
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ let rootFront;
+
+ client.connect().then(function onConnect() {
+ rootFront = client.mainRoot;
+
+ rootFront
+ .simpleReturn()
+ .then(
+ () => {
+ let stack = Components.stack;
+ while (stack) {
+ info(stack.name);
+ if (stack.name.includes("onConnect")) {
+ // Reached back to outer function before request
+ ok(true, "Complete stack");
+ return;
+ }
+ stack = stack.asyncCaller || stack.caller;
+ }
+ ok(false, "Incomplete stack");
+ },
+ () => {
+ ok(false, "Request failed unexpectedly");
+ }
+ )
+ .then(() => {
+ client.close().then(() => {
+ do_test_finished();
+ });
+ });
+ });
+
+ do_test_pending();
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_types.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_types.js
new file mode 100644
index 0000000000..4a62c5e073
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_types.js
@@ -0,0 +1,65 @@
+"use strict";
+
+const { types } = require("resource://devtools/shared/protocol.js");
+
+function run_test() {
+ types.addActorType("myActor1");
+ types.addActorType("myActor2");
+ types.addActorType("myActor3");
+
+ types.addPolymorphicType("ptype1", ["myActor1", "myActor2"]);
+ const ptype1 = types.getType("ptype1");
+ Assert.equal(ptype1.name, "ptype1");
+ Assert.equal(ptype1.category, "polymorphic");
+
+ types.addPolymorphicType("ptype2", ["myActor1", "myActor2", "myActor3"]);
+ const ptype2 = types.getType("ptype2");
+ Assert.equal(ptype2.name, "ptype2");
+ Assert.equal(ptype2.category, "polymorphic");
+
+ // Polymorphic types only accept actor types
+ try {
+ types.addPolymorphicType("ptype", ["myActor1", "myActor4"]);
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Unknown type: myActor4");
+ }
+ try {
+ types.addPolymorphicType("ptype", ["myActor1", "string"]);
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'myActor1,string', the type 'string' isn't an actor"
+ );
+ }
+ try {
+ types.addPolymorphicType("ptype", ["myActor1", "boolean"]);
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'myActor1,boolean', the type 'boolean' isn't an actor"
+ );
+ }
+
+ // Polymorphic types are not compatible with array or nullables
+ try {
+ types.addPolymorphicType("ptype", ["array:myActor1", "myActor2"]);
+ Assert.ok(false, "addType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'array:myActor1,myActor2', the type 'array:myActor1' isn't an actor"
+ );
+ }
+ try {
+ types.addPolymorphicType("ptype", ["nullable:myActor1", "myActor2"]);
+ Assert.ok(false, "addType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'nullable:myActor1,myActor2', the type 'nullable:myActor1' isn't an actor"
+ );
+ }
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js
new file mode 100644
index 0000000000..060a1743b1
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const { types } = require("resource://devtools/shared/protocol.js");
+
+function run_test() {
+ types.addType("test", {
+ read: v => "successful read: " + v,
+ write: v => "successful write: " + v,
+ });
+
+ // Verify the type registered correctly.
+
+ const type = types.getType("test");
+ const arrayType = types.getType("array:test");
+ Assert.equal(type.read("foo"), "successful read: foo");
+ Assert.equal(arrayType.read(["foo"])[0], "successful read: foo");
+
+ types.removeType("test");
+
+ Assert.equal(type.name, "DEFUNCT:test");
+ try {
+ types.getType("test");
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Unknown type: test");
+ }
+
+ try {
+ type.read("foo");
+ Assert.ok(false, "type.read should have thrown an exception.");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Using defunct type: test");
+ }
+
+ try {
+ arrayType.read(["foo"]);
+ Assert.ok(false, "array:test.read should have thrown an exception.");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Using defunct type: test");
+ }
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js
new file mode 100644
index 0000000000..4cc158579a
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test Front.watchFronts method.
+ */
+
+const protocol = require("resource://devtools/shared/protocol.js");
+const { RetVal } = protocol;
+
+const childSpec = protocol.generateActorSpec({
+ typeName: "childActor",
+
+ methods: {
+ release: {
+ release: true,
+ },
+ },
+});
+
+const ChildActor = protocol.ActorClassWithSpec(childSpec, {
+ initialize(conn, id) {
+ protocol.Actor.prototype.initialize.call(this, conn);
+ this.childID = id;
+ },
+
+ release() {},
+
+ form() {
+ return {
+ actor: this.actorID,
+ childID: this.childID,
+ foo: "bar",
+ };
+ },
+});
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ createChild: {
+ request: {},
+ response: { actor: RetVal("childActor") },
+ },
+ },
+});
+
+const RootActor = protocol.ActorClassWithSpec(rootSpec, {
+ typeName: "root",
+
+ initialize(conn) {
+ protocol.Actor.prototype.initialize.call(this, conn);
+
+ this.actorID = "root";
+
+ // Root actor owns itself.
+ this.manage(this);
+
+ this.sequence = 0;
+ },
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ },
+
+ createChild() {
+ return new ChildActor(this.conn, this.sequence++);
+ },
+});
+
+class ChildFront extends protocol.FrontClassWithSpec(childSpec) {
+ form(form) {
+ this.childID = form.childID;
+ this.foo = form.foo;
+ }
+}
+protocol.registerFront(ChildFront);
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function run_test() {
+ DevToolsServer.createRootActor = RootActor;
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ await client.connect();
+
+ const rootFront = client.mainRoot;
+
+ const fronts = [];
+ const listener = front => {
+ equal(
+ front.foo,
+ "bar",
+ "Front's form is set before watchFronts listeners are called"
+ );
+ fronts.push(front);
+ };
+ rootFront.watchFronts("childActor", listener);
+
+ const firstChild = await rootFront.createChild();
+ ok(
+ firstChild instanceof ChildFront,
+ "createChild returns a ChildFront instance"
+ );
+ equal(firstChild.childID, 0, "First child has ID=0");
+
+ equal(
+ fronts.length,
+ 1,
+ "watchFronts fires the callback, even if the front is created in the future"
+ );
+ equal(
+ fronts[0],
+ firstChild,
+ "watchFronts fires the callback with the right front instance"
+ );
+
+ const watchFrontsAfter = await new Promise(resolve => {
+ rootFront.watchFronts("childActor", resolve);
+ });
+ equal(
+ watchFrontsAfter,
+ firstChild,
+ "watchFronts fires the callback, even if the front is already created, " +
+ " with the same front instance"
+ );
+
+ equal(
+ fronts.length,
+ 1,
+ "There is still only one front reported from the first listener"
+ );
+
+ const secondChild = await rootFront.createChild();
+
+ equal(
+ fronts.length,
+ 2,
+ "After a second call to createChild, two fronts are reported"
+ );
+ equal(fronts[1], secondChild, "And the new front is the right instance");
+
+ // Test unregistering a front listener
+ rootFront.unwatchFronts("childActor", listener);
+
+ const thirdChild = await rootFront.createChild();
+ equal(
+ fronts.length,
+ 2,
+ "After calling unwatchFronts, the listener is no longer called"
+ );
+
+ // Test front destruction
+ const destroyed = [];
+ rootFront.watchFronts("childActor", null, front => {
+ destroyed.push(front);
+ });
+ await thirdChild.release();
+ equal(
+ destroyed.length,
+ 1,
+ "After the destruction of the front, one destruction is reported"
+ );
+ equal(destroyed[0], thirdChild, "And the destroyed front is the right one");
+
+ trace.close();
+ await client.close();
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/xpcshell.ini b/devtools/shared/protocol/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..9af8c5de49
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+
+[test_protocol_abort.js]
+[test_protocol_async.js]
+[test_protocol_children.js]
+[test_protocol_index.js]
+[test_protocol_invalid_response.js]
+[test_protocol_lifecycle.js]
+[test_protocol_longstring.js]
+[test_protocol_simple.js]
+[test_protocol_stack.js]
+[test_protocol_types.js]
+[test_protocol_unregister.js]
+[test_protocol_watchFronts.js]