diff options
Diffstat (limited to '')
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] |