author | Bill McCloskey <billm@mozilla.com> |
Thu, 19 Nov 2015 13:54:46 -0800 | |
changeset 309947 | 5c255680866961999617036aa5b6658908d111f2 |
parent 309946 | deda2ab537340b98f7cdceac54124f63821276d8 |
child 309948 | d648b84b5aa7bad8b12111fa7d4985772e4f9989 |
push id | 5513 |
push user | raliiev@mozilla.com |
push date | Mon, 25 Jan 2016 13:55:34 +0000 |
treeherder | mozilla-beta@5ee97dd05b5c [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | kmag |
bugs | 1208257 |
milestone | 45.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/Schemas.jsm @@ -0,0 +1,736 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; +const Cr = Components.results; + +this.EXPORTED_SYMBOLS = ["Schemas"]; + +Cu.import("resource://gre/modules/NetUtil.jsm"); + +function readJSON(uri) +{ + return new Promise((resolve, reject) => { + NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => { + if (!Components.isSuccessCode(status)) { + reject(new Error(status)); + return; + } + try { + let text = NetUtil.readInputStreamToString(inputStream, inputStream.available()); + + // Chrome JSON files include a license comment that we need to + // strip off for this to be valid JSON. As a hack, we just + // look for the first '[' character, which signals the start + // of the JSON content. + let index = text.indexOf("["); + text = text.slice(index); + + resolve(JSON.parse(text)); + } catch (e) { + reject(e); + } + }); + }); +} + +function getValueBaseType(value) +{ + let t = typeof(value); + if (t == "object") { + if (value === null) { + return "null"; + } else if (Array.isArray(value)) { + return "array"; + } else if (Object.prototype.toString.call(value) == "[object ArrayBuffer]") { + return "binary"; + } + } else if (t == "number") { + if (value % 1 == 0) { + return "integer"; + } + } + return t; +} + +// Schema files contain namespaces, and each namespace contains types, +// properties, functions, and events. An Entry is a base class for +// types, properties, functions, and events. +class Entry { + // Injects JS values for the entry into the extension API + // namespace. The default implementation is to do + // nothing. |wrapperFuncs| is used to call the actual implementation + // of a given function or event. It's an object with properties + // callFunction, addListener, removeListener, and hasListener. + inject(name, dest, wrapperFuncs) { + } +}; + +// Corresponds either to a type declared in the "types" section of the +// schema or else to any type object used throughout the schema. +class Type extends Entry { + // Takes a value, checks that it has the correct type, and returns a + // "normalized" version of the value. The normalized version will + // include "nulls" in place of omitted optional properties. The + // result of this function is either {error: "Some type error"} or + // {value: <normalized-value>}. + normalize(value) { + return {error: "invalid type"}; + } + + // Unlike normalize, this function does a shallow check to see if + // |baseType| (one of the possible getValueBaseType results) is + // valid for this type. It returns true or false. It's used to fill + // in optional arguments to functions before actually type checking + // the arguments. + checkBaseType(baseType) { + return false; + } + + // Helper method that simply relies on checkBaseType to implement + // normalize. Subclasses can choose to use it or not. + normalizeBase(type, value) { + if (this.checkBaseType(getValueBaseType(value))) { + return {value}; + } + return {error: `Expected ${type} instead of ${JSON.stringify(value)}`}; + } +}; + +// Type that allows any value. +class AnyType extends Type { + normalize(value) { + return {value}; + } + + checkBaseType(baseType) { + return true; + } +}; + +// An untagged union type. +class ChoiceType extends Type { + constructor(choices) { + super(); + this.choices = choices; + } + + normalize(value) { + for (let choice of this.choices) { + let r = choice.normalize(value); + if (!r.error) { + return r; + } + } + } + + checkBaseType(baseType) { + return this.choices.some(t => t.checkBaseType(baseType)); + } +}; + +// This is a reference to another type--essentially a typedef. +// FIXME +class RefType extends Type { + constructor(namespaceName, reference) { + super(); + this.namespaceName = namespaceName; + this.reference = reference; + } + + normalize(value) { + let ns = Schemas.namespaces.get(this.namespaceName); + let type = ns.get(this.reference); + if (!type) { + throw new Error(`Internal error: Type ${this.reference} not found`); + } + return type.normalize(value); + } + + checkBaseType(baseType) { + let ns = Schemas.namespaces.get(this.namespaceName); + let type = ns.get(this.reference); + if (!type) { + throw new Error(`Internal error: Type ${this.reference} not found`); + } + return type.checkBaseType(baseType); + } +}; + +class StringType extends Type { + constructor(enumeration, minLength, maxLength) { + super(); + this.enumeration = enumeration; + this.minLength = minLength; + this.maxLength = maxLength; + } + + normalize(value) { + let r = this.normalizeBase("string", value); + if (r.error) { + return r; + } + + if (this.enumeration) { + if (this.enumeration.includes(value)) { + return {value}; + } + return {error: `Invalid enumeration value ${JSON.stringify(value)}`}; + } + + if (value.length < this.minLength) { + return {error: `String ${JSON.stringify(value)} is too short (must be ${this.minLength})`}; + } + if (value.length > this.maxLength) { + return {error: `String ${JSON.stringify(value)} is too long (must be ${this.maxLength})`}; + } + + return r; + } + + checkBaseType(baseType) { + return baseType == "string"; + } + + inject(name, dest, wrapperFuncs) { + if (this.enumeration) { + let obj = Cu.createObjectIn(dest, {defineAs: name}); + for (let e of this.enumeration) { + let key = e.toUpperCase(); + obj[key] = e; + } + } + } +}; + +class UnrestrictedObjectType extends Type { + normalize(value) { + return this.normalizeBase("object", value); + } + + checkBaseType(baseType) { + return baseType == "object"; + } +}; + +class ObjectType extends Type { + constructor(properties, additionalProperties) { + super(); + this.properties = properties; + this.additionalProperties = additionalProperties; + } + + checkBaseType(baseType) { + return baseType == "object"; + } + + normalize(value) { + let v = this.normalizeBase("object", value); + if (v.error) { + return v; + } + + let result = {}; + for (let prop of Object.keys(this.properties)) { + let {type, optional, unsupported} = this.properties[prop]; + if (unsupported) { + if (prop in value) { + return {error: `Property "${prop}" is unsupported by Firefox`}; + } + } else if (prop in value) { + if (optional && (value[prop] === null || value[prop] === undefined)) { + result[prop] = null; + } else { + let r = type.normalize(value[prop]); + if (r.error) { + return r; + } + result[prop] = r.value; + } + } else if (!optional) { + return {error: `Property "${prop}" is required`}; + } else { + result[prop] = null; + } + } + + for (let prop of Object.keys(value)) { + if (!(prop in this.properties)) { + if (this.additionalProperties) { + let r = this.additionalProperties.normalize(value[prop]); + if (r.error) { + return r; + } + result[prop] = r.value; + } else { + return {error: `Unexpected property "${prop}"`}; + } + } + } + + return {value: result}; + } +}; + +class NumberType extends Type { + normalize(value) { + let r = this.normalizeBase("number", value); + if (r.error) { + return r; + } + + if (isNaN(value) || !Number.isFinite(value)) { + return {error: "NaN or infinity are not valid"}; + } + + return r; + } + + checkBaseType(baseType) { + return baseType == "number" || baseType == "integer"; + } +}; + +class IntegerType extends Type { + constructor(minimum, maximum) { + super(); + this.minimum = minimum; + this.maximum = maximum; + } + + normalize(value) { + let r = this.normalizeBase("integer", value); + if (r.error) { + return r; + } + + // Ensure it's between -2**31 and 2**31-1 + if ((value | 0) !== value) { + return {error: "Integer is out of range"}; + } + + if (value < this.minimum) { + return {error: `Integer ${value} is too small (must be at least ${this.minimum})`}; + } + if (value > this.maximum) { + return {error: `Integer ${value} is too big (must be at most ${this.maximum})`}; + } + + return r; + } + + checkBaseType(baseType) { + return baseType == "integer"; + } +}; + +class BooleanType extends Type { + normalize(value) { + return this.normalizeBase("boolean", value); + } + + checkBaseType(baseType) { + return baseType == "boolean"; + } +}; + +class ArrayType extends Type { + constructor(itemType) { + super(); + this.itemType = itemType; + } + + normalize(value) { + let v = this.normalizeBase("array", value); + if (v.error) { + return v; + } + + let result = []; + for (let element of value) { + element = this.itemType.normalize(element); + if (element.error) { + return element; + } + result.push(element.value); + } + + return {value: result}; + } + + checkBaseType(baseType) { + return baseType == "array"; + } +}; + +class FunctionType extends Type { + constructor(parameters) { + super(); + this.parameters = parameters; + } + + normalize(value) { + return this.normalizeBase("function", value); + } + + checkBaseType(baseType) { + return baseType == "function"; + } +}; + +// Represents a "property" defined in a schema namespace with a +// particular value. Essentially this is a constant. +class ValueProperty extends Entry { + constructor(name, value) { + super(); + this.name = name; + this.value = value; + } + + inject(name, dest, wrapperFuncs) { + dest[name] = this.value; + } +}; + +// Represents a "property" defined in a schema namespace that is not a +// constant. +class TypeProperty extends Entry { + constructor(name, type) { + super(); + this.name = name; + this.type = type; + } +}; + +// This class is a base class for FunctionEntrys and Events. It takes +// care of validating parameter lists (i.e., handling of optional +// parameters and parameter type checking). +class CallEntry extends Entry { + constructor(namespaceName, name, parameters) { + super(); + this.namespaceName = namespaceName; + this.name = name; + this.parameters = parameters; + } + + throwError(global, msg) { + global = Cu.getGlobalForObject(global); + throw new global.Error(`${msg} for ${this.namespaceName}.${this.name}.`); + } + + checkParameters(args, global) { + let fixedArgs = []; + + // First we create a new array, fixedArgs, that is the same as + // |args| but with null values in place of omitted optional + // parameters. + let check = (parameterIndex, argIndex) => { + if (parameterIndex == this.parameters.length) { + if (argIndex == args.length) { + return true; + } + return false; + } + + let parameter = this.parameters[parameterIndex]; + if (parameter.optional) { + // Try skipping it. + fixedArgs[parameterIndex] = null; + if (check(parameterIndex + 1, argIndex)) { + return true; + } + } + + if (argIndex == args.length) { + return false; + } + + let arg = args[argIndex]; + if (!parameter.type.checkBaseType(getValueBaseType(arg))) { + if (parameter.optional && (arg === null || arg === undefined)) { + fixedArgs[parameterIndex] = null; + } else { + return false; + } + } else { + fixedArgs[parameterIndex] = arg; + } + + return check(parameterIndex + 1, argIndex + 1); + } + + let success = check(0, 0); + if (!success) { + this.throwError(global, "Incorrect argument types"); + } + + // Now we normalize (and fully type check) all non-omitted arguments. + fixedArgs = fixedArgs.map((arg, parameterIndex) => { + if (arg === null) { + return null; + } else { + let parameter = this.parameters[parameterIndex]; + let r = parameter.type.normalize(arg); + if (r.error) { + this.throwError(global, `Type error for parameter ${parameter.name} (${r.error})`); + } + return r.value; + } + }); + + return fixedArgs; + } +}; + +// Represents a "function" defined in a schema namespace. +class FunctionEntry extends CallEntry { + constructor(namespaceName, name, type, unsupported) { + super(namespaceName, name, type.parameters); + this.unsupported = unsupported; + } + + inject(name, dest, wrapperFuncs) { + if (this.unsupported) { + return; + } + + let stub = (...args) => { + let actuals = this.checkParameters(args, dest); + return wrapperFuncs.callFunction(this.namespaceName, name, actuals); + } + Cu.exportFunction(stub, dest, {defineAs: name}); + } +}; + +// Represents an "event" defined in a schema namespace. +class Event extends CallEntry { + constructor(namespaceName, name, type, extraParameters, unsupported) { + super(namespaceName, name, extraParameters); + this.type = type; + this.unsupported = unsupported; + } + + checkListener(global, listener) { + let r = this.type.normalize(listener); + if (r.error) { + this.throwError(global, "Invalid listener"); + } + return r.value; + } + + inject(name, dest, wrapperFuncs) { + if (this.unsupported) { + return; + } + + let addStub = (listener, ...args) => { + listener = this.checkListener(dest, listener); + let actuals = this.checkParameters(args, dest); + return wrapperFuncs.addListener(this.namespaceName, name, listener, actuals); + }; + + let removeStub = (listener) => { + listener = this.checkListener(dest, listener); + return wrapperFuncs.removeListener(this.namespaceName, name, listener); + }; + + let hasStub = (listener) => { + listener = this.checkListener(dest, listener); + return wrapperFuncs.hasListener(this.namespaceName, name, listener); + }; + + let obj = Cu.createObjectIn(dest, {defineAs: name}); + Cu.exportFunction(addStub, obj, {defineAs: "addListener"}); + Cu.exportFunction(removeStub, obj, {defineAs: "removeListener"}); + Cu.exportFunction(hasStub, obj, {defineAs: "hasListener"}); + } +}; + +this.Schemas = { + // Map[<schema-name> -> Map[<symbol-name> -> Entry]] + // This keeps track of all the schemas that have been loaded so far. + namespaces: new Map(), + + register(namespaceName, symbol, value) { + let ns = this.namespaces.get(namespaceName); + if (!ns) { + ns = new Map(); + this.namespaces.set(namespaceName, ns); + } + ns.set(symbol, value); + }, + + parseType(namespaceName, type, extraProperties = []) { + let allowedProperties = new Set(extraProperties); + + // Do some simple validation of our own schemas. + function checkTypeProperties(...extra) { + let allowedSet = new Set([...allowedProperties, ...extra, "description"]); + for (let prop of Object.keys(type)) { + if (!allowedSet.has(prop)) { + throw new Error(`Internal error: Namespace ${namespaceName} has invalid type property ${prop} in type ${type.name}`); + } + } + }; + + if ("choices" in type) { + checkTypeProperties("choices"); + + let choices = type.choices.map(t => this.parseType(namespaceName, t)); + return new ChoiceType(choices); + } else if ("$ref" in type) { + checkTypeProperties("$ref"); + return new RefType(namespaceName, type["$ref"]); + } + + if (!("type" in type)) { + throw new Error(`Unexpected value for type: ${JSON.stringify(type)}`); + } + + allowedProperties.add("type"); + + // Otherwise it's a normal type... + if (type.type == "string") { + checkTypeProperties("enum", "minLength", "maxLength"); + return new StringType(type["enum"] || null, + type.minLength || 0, + type.maxLength || Infinity); + } else if (type.type == "object") { + if (!type.properties) { + checkTypeProperties(); + return new UnrestrictedObjectType(); + } + let properties = {}; + for (let propName of Object.keys(type.properties)) { + properties[propName] = { + type: this.parseType(namespaceName, type.properties[propName], ["optional", "unsupported"]), + optional: type.properties[propName].optional || false, + unsupported: type.properties[propName].unsupported || false, + }; + } + + let additionalProperties = null; + if ("additionalProperties" in type) { + additionalProperties = this.parseType(namespaceName, type.additionalProperties); + } + + return new ObjectType(properties, additionalProperties); + } else if (type.type == "array") { + checkTypeProperties("items"); + return new ArrayType(this.parseType(namespaceName, type.items)); + } else if (type.type == "number") { + checkTypeProperties(); + return new NumberType(); + } else if (type.type == "integer") { + checkTypeProperties("minimum", "maximum"); + return new IntegerType(type.minimum || 0, type.maximum || Infinity); + } else if (type.type == "boolean") { + checkTypeProperties(); + return new BooleanType(); + } else if (type.type == "function") { + let parameters = null; + if ("parameters" in type) { + parameters = []; + for (let param of type.parameters) { + parameters.push({ + type: this.parseType(namespaceName, param, ["name", "optional"]), + name: param.name, + optional: param.optional || false, + }); + } + } + + checkTypeProperties("parameters"); + return new FunctionType(parameters); + } else if (type.type == "any") { + checkTypeProperties(); + return new AnyType(); + } else { + throw new Error(`Unexpected type ${type.type}`); + } + }, + + loadType(namespaceName, type) { + this.register(namespaceName, type.id, this.parseType(namespaceName, type, ["id"])); + }, + + loadProperty(namespaceName, name, prop) { + if ("value" in prop) { + this.register(namespaceName, name, new ValueProperty(name, prop.value)); + } else { + let type = this.parseType(namespaceName, prop); + this.register(namespaceName, name, new TypeProperty(name, type)); + } + }, + + loadFunction(namespaceName, fun) { + let f = new FunctionEntry(namespaceName, fun.name, + this.parseType(namespaceName, fun, ["name", "unsupported"]), + fun.unsupported || false); + this.register(namespaceName, fun.name, f); + }, + + loadEvent(namespaceName, event) { + let extras = event.extraParameters || []; + extras = extras.map(param => { + return { + type: this.parseType(namespaceName, param, ["name", "optional"]), + name: param.name, + optional: param.optional || false, + }; + }); + + // We ignore these properties for now. + let returns = event.returns; + let filters = event.filters; + + let type = this.parseType(namespaceName, event, + ["name", "unsupported", "extraParameters", "returns", "filters"]); + + let e = new Event(namespaceName, event.name, type, extras, + event.unsupported || false); + this.register(namespaceName, event.name, e); + }, + + load(uri) { + return readJSON(uri).then(json => { + for (let namespace of json) { + let name = namespace.namespace; + + let types = namespace.types || []; + for (let type of types) { + this.loadType(name, type); + } + + let properties = namespace.properties || {}; + for (let propertyName of Object.keys(properties)) { + this.loadProperty(name, propertyName, properties[propertyName]); + } + + let functions = namespace.functions || []; + for (let fun of functions) { + this.loadFunction(name, fun); + } + + let events = namespace.events || []; + for (let event of events) { + this.loadEvent(name, event); + } + } + }); + }, + + inject(dest, wrapperFuncs) { + for (let [namespace, ns] of this.namespaces) { + let obj = Cu.createObjectIn(dest, {defineAs: namespace}); + for (let [name, entry] of ns) { + entry.inject(name, obj, wrapperFuncs); + } + } + }, +};
--- a/toolkit/components/extensions/moz.build +++ b/toolkit/components/extensions/moz.build @@ -5,15 +5,15 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXTRA_JS_MODULES += [ 'Extension.jsm', 'ExtensionContent.jsm', 'ExtensionManagement.jsm', 'ExtensionStorage.jsm', 'ExtensionUtils.jsm', + 'Schemas.jsm', ] JAR_MANIFESTS += ['jar.mn'] MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini'] - XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js @@ -0,0 +1,320 @@ +"use strict"; + +Components.utils.import("resource://gre/modules/Schemas.jsm"); +Components.utils.import("resource://gre/modules/BrowserUtils.jsm"); + +let json = [ + {namespace: "testing", + + properties: { + PROP1: {value: 20}, + prop2: {type: "string"}, + }, + + types: [ + { + id: "type1", + type: "string", + "enum": ["value1", "value2", "value3"], + }, + + { + id: "type2", + type: "object", + properties: { + prop1: {type: "integer"}, + prop2: {type: "array", items: {"$ref": "type1"}}, + }, + } + ], + + functions: [ + { + name: "foo", + type: "function", + parameters: [ + {name: "arg1", type: "integer", optional: true}, + {name: "arg2", type: "boolean", optional: true}, + ], + }, + + { + name: "bar", + type: "function", + parameters: [ + {name: "arg1", type: "integer", optional: true}, + {name: "arg2", type: "boolean"}, + ], + }, + + { + name: "baz", + type: "function", + parameters: [ + {name: "arg1", type: "object", properties: { + prop1: {type: "string"}, + prop2: {type: "integer", optional: true}, + prop3: {type: "integer", unsupported: true}, + }}, + ], + }, + + { + name: "qux", + type: "function", + parameters: [ + {name: "arg1", "$ref": "type1"}, + ], + }, + + { + name: "quack", + type: "function", + parameters: [ + {name: "arg1", "$ref": "type2"}, + ], + }, + + { + name: "quora", + type: "function", + parameters: [ + {name: "arg1", type: "function"}, + ], + }, + + { + name: "quileute", + type: "function", + parameters: [ + {name: "arg1", type: "integer", optional: true}, + {name: "arg2", type: "integer"}, + ], + }, + + { + name: "queets", + type: "function", + unsupported: true, + parameters: [], + }, + + { + name: "quintuplets", + type: "function", + parameters: [ + {name: "obj", type: "object", properties: [], additionalProperties: {type: "integer"}}, + ], + }, + + { + name: "quasar", + type: "function", + parameters: [ + {name: "abc", type: "object", properties: { + func: {type: "function", parameters: [ + {name: "x", type: "integer"} + ]} + }} + ], + }, + + { + name: "quosimodo", + type: "function", + parameters: [ + {name: "xyz", type: "object"}, + ], + }, + ], + + events: [ + { + name: "onFoo", + type: "function", + }, + + { + name: "onBar", + type: "function", + extraParameters: [{ + name: "filter", + type: "integer", + }], + } + ], + } +]; + +let tallied = null; + +function tally(kind, ns, name, args) { + tallied = [kind, ns, name, args]; +} + +function verify(...args) { + do_check_eq(JSON.stringify(tallied), JSON.stringify(args)); + tallied = null; +} + +let wrapper = { + callFunction(ns, name, args) { + tally("call", ns, name, args); + }, + + addListener(ns, name, listener, args) { + tally("addListener", ns, name, [listener, args]); + }, + removeListener(ns, name, listener) { + tally("removeListener", ns, name, [listener]); + }, + hasListener(ns, name, listener) { + tally("hasListener", ns, name, [listener]); + }, +}; + +add_task(function* () { + let url = "data:," + JSON.stringify(json); + let uri = BrowserUtils.makeURI(url); + yield Schemas.load(uri); + + let root = {}; + Schemas.inject(root, wrapper); + + do_check_eq(root.testing.PROP1, 20, "simple value property"); + do_check_eq(root.testing.type1.VALUE1, "value1", "enum type"); + do_check_eq(root.testing.type1.VALUE2, "value2", "enum type"); + + root.testing.foo(11, true); + verify("call", "testing", "foo", [11, true]); + + root.testing.foo(true); + verify("call", "testing", "foo", [null, true]); + + root.testing.foo(null, true); + verify("call", "testing", "foo", [null, true]); + + root.testing.foo(undefined, true); + verify("call", "testing", "foo", [null, true]); + + root.testing.foo(11); + verify("call", "testing", "foo", [11, null]); + + Assert.throws(() => root.testing.bar(11), + /Incorrect argument types/, + "should throw without required arg"); + + Assert.throws(() => root.testing.bar(11, true, 10), + /Incorrect argument types/, + "should throw with too many arguments"); + + root.testing.bar(true); + verify("call", "testing", "bar", [null, true]); + + root.testing.baz({ prop1: "hello", prop2: 22 }); + verify("call", "testing", "baz", [{ prop1: "hello", prop2: 22 }]); + + root.testing.baz({ prop1: "hello" }); + verify("call", "testing", "baz", [{ prop1: "hello", prop2: null }]); + + root.testing.baz({ prop1: "hello", prop2: null }); + verify("call", "testing", "baz", [{ prop1: "hello", prop2: null }]); + + Assert.throws(() => root.testing.baz({ prop2: 12 }), + /Property "prop1" is required/, + "should throw without required property"); + + Assert.throws(() => root.testing.baz({ prop1: "hi", prop3: 12 }), + /Property "prop3" is unsupported by Firefox/, + "should throw with unsupported property"); + + Assert.throws(() => root.testing.baz({ prop1: "hi", prop4: 12 }), + /Unexpected property "prop4"/, + "should throw with unexpected property"); + + Assert.throws(() => root.testing.baz({ prop1: 12 }), + /Expected string instead of 12/, + "should throw with wrong type"); + + root.testing.qux("value2"); + verify("call", "testing", "qux", ["value2"]); + + Assert.throws(() => root.testing.qux("value4"), + /Invalid enumeration value "value4"/, + "should throw for invalid enum value"); + + root.testing.quack({prop1: 12, prop2: ["value1", "value3"]}); + verify("call", "testing", "quack", [{prop1: 12, prop2: ["value1", "value3"]}]); + + Assert.throws(() => root.testing.quack({prop1: 12, prop2: ["value1", "value3", "value4"]}), + /Invalid enumeration value "value4"/, + "should throw for invalid array type"); + + function f() {} + root.testing.quora(f); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"])); + do_check_eq(tallied[3][0], f); + tallied = null; + + let g = () => 0; + root.testing.quora(g); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"])); + do_check_eq(tallied[3][0], g); + tallied = null; + + root.testing.quileute(10); + verify("call", "testing", "quileute", [null, 10]); + + Assert.throws(() => root.testing.queets(), + /queets is not a function/, + "should throw for unsupported functions"); + + root.testing.quintuplets({a: 10, b: 20, c: 30}); + verify("call", "testing", "quintuplets", [{a: 10, b: 20, c: 30}]); + + Assert.throws(() => root.testing.quintuplets({a: 10, b: 20, c: 30, d: "hi"}), + /Expected integer instead of "hi"/, + "should throw for wrong additionalProperties type"); + + root.testing.quasar({func: f}); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quasar"])); + do_check_eq(tallied[3][0].func, f); + tallied = null; + + root.testing.quosimodo({a: 10, b: 20, c: 30}); + verify("call", "testing", "quosimodo", [{a: 10, b: 20, c: 30}]); + + Assert.throws(() => root.testing.quosimodo(10), + /Incorrect argument types/, + "should throw for wrong type"); + + root.testing.onFoo.addListener(f); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onFoo"])); + do_check_eq(tallied[3][0], f); + do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([])); + tallied = null; + + root.testing.onFoo.removeListener(f); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["removeListener", "testing", "onFoo"])); + do_check_eq(tallied[3][0], f); + tallied = null; + + root.testing.onFoo.hasListener(f); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["hasListener", "testing", "onFoo"])); + do_check_eq(tallied[3][0], f); + tallied = null; + + Assert.throws(() => root.testing.onFoo.addListener(10), + /Invalid listener/, + "addListener with non-function should throw"); + + root.testing.onBar.addListener(f, 10); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"])); + do_check_eq(tallied[3][0], f); + do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([10])); + tallied = null; + + Assert.throws(() => root.testing.onBar.addListener(f, "hi"), + /Incorrect argument types/, + "addListener with wrong extra parameter should throw"); +});