diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js index 21d69d21660c..d6d283336b21 100644 --- a/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js +++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js @@ -21,6 +21,8 @@ function simpleHello() { // 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", @@ -200,6 +202,15 @@ class ChildFront extends protocol.FrontClassWithSpec(childSpec) { } protocol.registerFront(ChildFront); +const otherChildSpec = protocol.generateActorSpec({ + typeName: "otherChildActor", + methods: {}, + events: {}, +}); +const OtherChildActor = protocol.ActorClassWithSpec(otherChildSpec, {}); +class OtherChildFront extends protocol.FrontClassWithSpec(otherChildSpec) {} +protocol.registerFront(OtherChildFront); + types.addDictType("manyChildrenDict", { child5: "childActor", more: "array:childActor", @@ -230,6 +241,17 @@ const rootSpec = protocol.generateActorSpec({ request: { id: Arg(0) }, response: { child: RetVal("temp:childActor") }, }, + getPolymorphism: { + request: { id: Arg(0, "number") }, + response: { child: RetVal("polytype") }, + }, + requestPolymorphism: { + request: { + id: Arg(0, "number"), + actor: Arg(1, "polytype"), + }, + response: { child: RetVal("polytype") }, + }, clearTemporaryChildren: {}, }, }); @@ -296,6 +318,24 @@ const RootActor = protocol.ActorClassWithSpec(rootSpec, { this._temporaryHolder.destroy(); delete this._temporaryHolder; }, + + getPolymorphism: function(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: function(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) { @@ -363,6 +403,7 @@ add_task(async function() { await testEvents(trace); await testManyChildren(trace); await testGenerator(trace); + await testPolymorphism(trace); await client.close(); }); @@ -641,3 +682,33 @@ async function testGenerator(trace) { 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'/); +} 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 000000000000..f4ecdb63b40a --- /dev/null +++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_types.js @@ -0,0 +1,65 @@ +"use strict"; + +const { types } = require("devtools/shared/protocol"); + +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/xpcshell.ini b/devtools/shared/protocol/tests/xpcshell/xpcshell.ini index 815a81ec23af..d4f595804215 100644 --- a/devtools/shared/protocol/tests/xpcshell/xpcshell.ini +++ b/devtools/shared/protocol/tests/xpcshell/xpcshell.ini @@ -12,5 +12,6 @@ support-files = [test_protocol_longstring.js] [test_protocol_simple.js] [test_protocol_stack.js] +[test_protocol_types.js] [test_protocol_unregister.js] [test_protocol_watchFronts.js] diff --git a/devtools/shared/protocol/types.js b/devtools/shared/protocol/types.js index ddc8e6cde242..77f8bd768640 100644 --- a/devtools/shared/protocol/types.js +++ b/devtools/shared/protocol/types.js @@ -370,6 +370,68 @@ types.addActorType = function(name) { return type; }; +types.addPolymorphicType = function(name, subtypes) { + // Assert that all subtypes are actors, as the marshalling implementation depends on that. + for (const subTypeName of subtypes) { + const subtype = types.getType(subTypeName); + if (subtype.category != "actor") { + throw new Error( + `In polymorphic type '${subtypes.join( + "," + )}', the type '${subTypeName}' isn't an actor` + ); + } + } + + return types.addType(name, { + category: "polymorphic", + read: (value, ctx) => { + // `value` is either a string which is an Actor ID or a form object + // where `actor` is an actor ID + const actorID = typeof value === "string" ? value : value.actor; + if (!actorID) { + throw new Error( + `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'` + ); + } + + // Extract the typeName out of the actor ID, which should be composed like this + // ${DebuggerServerConnectionPrefix}.${typeName}${Number} + const typeName = actorID.match(/\.([a-zA-Z]+)\d+$/)[1]; + if (!subtypes.includes(typeName)) { + throw new Error( + `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'` + ); + } + + const subtype = types.getType(typeName); + return subtype.read(value, ctx); + }, + write: (value, ctx) => { + if (!value) { + throw new Error( + `Was expecting one of these actors '${subtypes}' but instead got an empty value.` + ); + } + // value is either an `Actor` or a `Front` and both classes exposes a `typeName` + const typeName = value.typeName; + if (!typeName) { + throw new Error( + `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'. Did you pass a form instead of an Actor?` + ); + } + + if (!subtypes.includes(typeName)) { + throw new Error( + `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'` + ); + } + + const subtype = types.getType(typeName); + return subtype.write(value, ctx); + }, + }); +}; types.addNullableType = function(subtype) { subtype = types.getType(subtype); return types.addType("nullable:" + subtype.name, {