UNPKG

wpt-runner

Version:

Runs web platform tests in Node.js using jsdom

1,431 lines (1,280 loc) 145 kB
/* For user documentation see docs/_writing-tests/idlharness.md */ /** * Notes for people who want to edit this file (not just use it as a library): * * Most of the interesting stuff happens in the derived classes of IdlObject, * especially IdlInterface. The entry point for all IdlObjects is .test(), * which is called by IdlArray.test(). An IdlObject is conceptually just * "thing we want to run tests on", and an IdlArray is an array of IdlObjects * with some additional data thrown in. * * The object model is based on what WebIDLParser.js produces, which is in turn * based on its pegjs grammar. If you want to figure out what properties an * object will have from WebIDLParser.js, the best way is to look at the * grammar: * * https://github.com/darobin/webidl.js/blob/master/lib/grammar.peg * * So for instance: * * // interface definition * interface * = extAttrs:extendedAttributeList? S? "interface" S name:identifier w herit:ifInheritance? w "{" w mem:ifMember* w "}" w ";" w * { return { type: "interface", name: name, inheritance: herit, members: mem, extAttrs: extAttrs }; } * * This means that an "interface" object will have a .type property equal to * the string "interface", a .name property equal to the identifier that the * parser found, an .inheritance property equal to either null or the result of * the "ifInheritance" production found elsewhere in the grammar, and so on. * After each grammatical production is a JavaScript function in curly braces * that gets called with suitable arguments and returns some JavaScript value. * * (Note that the version of WebIDLParser.js we use might sometimes be * out-of-date or forked.) * * The members and methods of the classes defined by this file are all at least * briefly documented, hopefully. */ (function(){ "use strict"; // Support subsetTestByKey from /common/subset-tests-by-key.js, but make it optional if (!('subsetTestByKey' in self)) { self.subsetTestByKey = function(key, callback, ...args) { return callback(...args); } self.shouldRunSubTest = () => true; } /// Helpers /// function constValue (cnt) { if (cnt.type === "null") return null; if (cnt.type === "NaN") return NaN; if (cnt.type === "Infinity") return cnt.negative ? -Infinity : Infinity; if (cnt.type === "number") return +cnt.value; return cnt.value; } function minOverloadLength(overloads) { // "The value of the Function object’s “length” property is // a Number determined as follows: // ". . . // "Return the length of the shortest argument list of the // entries in S." if (!overloads.length) { return 0; } return overloads.map(function(attr) { return attr.arguments ? attr.arguments.filter(function(arg) { return !arg.optional && !arg.variadic; }).length : 0; }) .reduce(function(m, n) { return Math.min(m, n); }); } // A helper to get the global of a Function object. This is needed to determine // which global exceptions the function throws will come from. function globalOf(func) { try { // Use the fact that .constructor for a Function object is normally the // Function constructor, which can be used to mint a new function in the // right global. return func.constructor("return this;")(); } catch (e) { } // If the above fails, because someone gave us a non-function, or a function // with a weird proto chain or weird .constructor property, just fall back // to 'self'. return self; } // https://esdiscuss.org/topic/isconstructor#content-11 function isConstructor(o) { try { new (new Proxy(o, {construct: () => ({})})); return true; } catch(e) { return false; } } function throwOrReject(a_test, operation, fn, obj, args, message, cb) { if (operation.idlType.generic !== "Promise") { assert_throws_js(globalOf(fn).TypeError, function() { fn.apply(obj, args); }, message); cb(); } else { try { promise_rejects_js(a_test, TypeError, fn.apply(obj, args), message).then(cb, cb); } catch (e){ a_test.step(function() { assert_unreached("Throws \"" + e + "\" instead of rejecting promise"); cb(); }); } } } function awaitNCallbacks(n, cb, ctx) { var counter = 0; return function() { counter++; if (counter >= n) { cb(); } }; } /// IdlHarnessError /// // Entry point self.IdlHarnessError = function(message) { /** * Message to be printed as the error's toString invocation. */ this.message = message; }; IdlHarnessError.prototype = Object.create(Error.prototype); IdlHarnessError.prototype.toString = function() { return this.message; }; /// IdlArray /// // Entry point self.IdlArray = function() { /** * A map from strings to the corresponding named IdlObject, such as * IdlInterface or IdlException. These are the things that test() will run * tests on. */ this.members = {}; /** * A map from strings to arrays of strings. The keys are interface or * exception names, and are expected to also exist as keys in this.members * (otherwise they'll be ignored). This is populated by add_objects() -- * see documentation at the start of the file. The actual tests will be * run by calling this.members[name].test_object(obj) for each obj in * this.objects[name]. obj is a string that will be eval'd to produce a * JavaScript value, which is supposed to be an object implementing the * given IdlObject (interface, exception, etc.). */ this.objects = {}; /** * When adding multiple collections of IDLs one at a time, an earlier one * might contain a partial interface or includes statement that depends * on a later one. Save these up and handle them right before we run * tests. * * Both this.partials and this.includes will be the objects as parsed by * WebIDLParser.js, not wrapped in IdlInterface or similar. */ this.partials = []; this.includes = []; /** * Record of skipped IDL items, in case we later realize that they are a * dependency (to retroactively process them). */ this.skipped = new Map(); }; IdlArray.prototype.add_idls = function(raw_idls, options) { /** Entry point. See documentation at beginning of file. */ this.internal_add_idls(WebIDL2.parse(raw_idls), options); }; IdlArray.prototype.add_untested_idls = function(raw_idls, options) { /** Entry point. See documentation at beginning of file. */ var parsed_idls = WebIDL2.parse(raw_idls); this.mark_as_untested(parsed_idls); this.internal_add_idls(parsed_idls, options); }; IdlArray.prototype.mark_as_untested = function (parsed_idls) { for (var i = 0; i < parsed_idls.length; i++) { parsed_idls[i].untested = true; if ("members" in parsed_idls[i]) { for (var j = 0; j < parsed_idls[i].members.length; j++) { parsed_idls[i].members[j].untested = true; } } } }; IdlArray.prototype.is_excluded_by_options = function (name, options) { return options && (options.except && options.except.includes(name) || options.only && !options.only.includes(name)); }; IdlArray.prototype.add_dependency_idls = function(raw_idls, options) { return this.internal_add_dependency_idls(WebIDL2.parse(raw_idls), options); }; IdlArray.prototype.internal_add_dependency_idls = function(parsed_idls, options) { const new_options = { only: [] } const all_deps = new Set(); Object.values(this.members).forEach(v => { if (v.base) { all_deps.add(v.base); } }); // Add both 'A' and 'B' for each 'A includes B' entry. this.includes.forEach(i => { all_deps.add(i.target); all_deps.add(i.includes); }); this.partials.forEach(p => all_deps.add(p.name)); // Add 'TypeOfType' for each "typedef TypeOfType MyType;" entry. Object.entries(this.members).forEach(([k, v]) => { if (v instanceof IdlTypedef) { let defs = v.idlType.union ? v.idlType.idlType.map(t => t.idlType) : [v.idlType.idlType]; defs.forEach(d => all_deps.add(d)); } }); // Add the attribute idlTypes of all the nested members of idls. const attrDeps = parsedIdls => { return parsedIdls.reduce((deps, parsed) => { if (parsed.members) { for (const attr of Object.values(parsed.members).filter(m => m.type === 'attribute')) { let attrType = attr.idlType; // Check for generic members (e.g. FrozenArray<MyType>) if (attrType.generic) { deps.add(attrType.generic); attrType = attrType.idlType; } deps.add(attrType.idlType); } } if (parsed.base in this.members) { attrDeps([this.members[parsed.base]]).forEach(dep => deps.add(dep)); } return deps; }, new Set()); }; const testedMembers = Object.values(this.members).filter(m => !m.untested && m.members); attrDeps(testedMembers).forEach(dep => all_deps.add(dep)); const testedPartials = this.partials.filter(m => !m.untested && m.members); attrDeps(testedPartials).forEach(dep => all_deps.add(dep)); if (options && options.except && options.only) { throw new IdlHarnessError("The only and except options can't be used together."); } const defined_or_untested = name => { // NOTE: Deps are untested, so we're lenient, and skip re-encountered definitions. // e.g. for 'idl' containing A:B, B:C, C:D // array.add_idls(idl, {only: ['A','B']}). // array.add_dependency_idls(idl); // B would be encountered as tested, and encountered as a dep, so we ignore. return name in this.members || this.is_excluded_by_options(name, options); } // Maps name -> [parsed_idl, ...] const process = function(parsed) { var deps = []; if (parsed.name) { deps.push(parsed.name); } else if (parsed.type === "includes") { deps.push(parsed.target); deps.push(parsed.includes); } deps = deps.filter(function(name) { if (!name || name === parsed.name && defined_or_untested(name) || !all_deps.has(name)) { // Flag as skipped, if it's not already processed, so we can // come back to it later if we retrospectively call it a dep. if (name && !(name in this.members)) { this.skipped.has(name) ? this.skipped.get(name).push(parsed) : this.skipped.set(name, [parsed]); } return false; } return true; }.bind(this)); deps.forEach(function(name) { if (!new_options.only.includes(name)) { new_options.only.push(name); } const follow_up = new Set(); for (const dep_type of ["inheritance", "includes"]) { if (parsed[dep_type]) { const inheriting = parsed[dep_type]; const inheritor = parsed.name || parsed.target; const deps = [inheriting]; // For A includes B, we can ignore A, unless B (or some of its // members) is being tested. if (dep_type !== "includes" || inheriting in this.members && !this.members[inheriting].untested || this.partials.some(function(p) { return p.name === inheriting; })) { deps.push(inheritor); } for (const dep of deps) { if (!new_options.only.includes(dep)) { new_options.only.push(dep); } all_deps.add(dep); follow_up.add(dep); } } } for (const deferred of follow_up) { if (this.skipped.has(deferred)) { const next = this.skipped.get(deferred); this.skipped.delete(deferred); next.forEach(process); } } }.bind(this)); }.bind(this); for (let parsed of parsed_idls) { process(parsed); } this.mark_as_untested(parsed_idls); if (new_options.only.length) { this.internal_add_idls(parsed_idls, new_options); } } IdlArray.prototype.internal_add_idls = function(parsed_idls, options) { /** * Internal helper called by add_idls() and add_untested_idls(). * * parsed_idls is an array of objects that come from WebIDLParser.js's * "definitions" production. The add_untested_idls() entry point * additionally sets an .untested property on each object (and its * .members) so that they'll be skipped by test() -- they'll only be * used for base interfaces of tested interfaces, return types, etc. * * options is a dictionary that can have an only or except member which are * arrays. If only is given then only members, partials and interface * targets listed will be added, and if except is given only those that * aren't listed will be added. Only one of only and except can be used. */ if (options && options.only && options.except) { throw new IdlHarnessError("The only and except options can't be used together."); } var should_skip = name => { return this.is_excluded_by_options(name, options); } parsed_idls.forEach(function(parsed_idl) { var partial_types = [ "interface", "interface mixin", "dictionary", "namespace", ]; if (parsed_idl.partial && partial_types.includes(parsed_idl.type)) { if (should_skip(parsed_idl.name)) { return; } this.partials.push(parsed_idl); return; } if (parsed_idl.type == "includes") { if (should_skip(parsed_idl.target)) { return; } this.includes.push(parsed_idl); return; } parsed_idl.array = this; if (should_skip(parsed_idl.name)) { return; } if (parsed_idl.name in this.members) { throw new IdlHarnessError("Duplicate identifier " + parsed_idl.name); } switch(parsed_idl.type) { case "interface": this.members[parsed_idl.name] = new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ false); break; case "interface mixin": this.members[parsed_idl.name] = new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ true); break; case "dictionary": // Nothing to test, but we need the dictionary info around for type // checks this.members[parsed_idl.name] = new IdlDictionary(parsed_idl); break; case "typedef": this.members[parsed_idl.name] = new IdlTypedef(parsed_idl); break; case "callback": this.members[parsed_idl.name] = new IdlCallback(parsed_idl); break; case "enum": this.members[parsed_idl.name] = new IdlEnum(parsed_idl); break; case "callback interface": this.members[parsed_idl.name] = new IdlInterface(parsed_idl, /* is_callback = */ true, /* is_mixin = */ false); break; case "namespace": this.members[parsed_idl.name] = new IdlNamespace(parsed_idl); break; default: throw parsed_idl.name + ": " + parsed_idl.type + " not yet supported"; } }.bind(this)); }; IdlArray.prototype.add_objects = function(dict) { /** Entry point. See documentation at beginning of file. */ for (var k in dict) { if (k in this.objects) { this.objects[k] = this.objects[k].concat(dict[k]); } else { this.objects[k] = dict[k]; } } }; IdlArray.prototype.prevent_multiple_testing = function(name) { /** Entry point. See documentation at beginning of file. */ this.members[name].prevent_multiple_testing = true; }; IdlArray.prototype.is_json_type = function(type) { /** * Checks whether type is a JSON type as per * https://webidl.spec.whatwg.org/#dfn-json-types */ var idlType = type.idlType; if (type.generic == "Promise") { return false; } // nullable and annotated types don't need to be handled separately, // as webidl2 doesn't represent them wrapped-up (as they're described // in WebIDL). // union and record types if (type.union || type.generic == "record") { return idlType.every(this.is_json_type, this); } // sequence types if (type.generic == "sequence" || type.generic == "FrozenArray") { return this.is_json_type(idlType[0]); } if (typeof idlType != "string") { throw new Error("Unexpected type " + JSON.stringify(idlType)); } switch (idlType) { // Numeric types case "byte": case "octet": case "short": case "unsigned short": case "long": case "unsigned long": case "long long": case "unsigned long long": case "float": case "double": case "unrestricted float": case "unrestricted double": // boolean case "boolean": // string types case "DOMString": case "ByteString": case "USVString": // object type case "object": return true; case "Error": case "DOMException": case "Int8Array": case "Int16Array": case "Int32Array": case "Uint8Array": case "Uint16Array": case "Uint32Array": case "Uint8ClampedArray": case "BigInt64Array": case "BigUint64Array": case "Float32Array": case "Float64Array": case "ArrayBuffer": case "DataView": case "any": return false; default: var thing = this.members[idlType]; if (!thing) { throw new Error("Type " + idlType + " not found"); } if (thing instanceof IdlEnum) { return true; } if (thing instanceof IdlTypedef) { return this.is_json_type(thing.idlType); } // dictionaries where all of their members are JSON types if (thing instanceof IdlDictionary) { const map = new Map(); for (const dict of thing.get_reverse_inheritance_stack()) { for (const m of dict.members) { map.set(m.name, m.idlType); } } return Array.from(map.values()).every(this.is_json_type, this); } // interface types that have a toJSON operation declared on themselves or // one of their inherited interfaces. if (thing instanceof IdlInterface) { var base; while (thing) { if (thing.has_to_json_regular_operation()) { return true; } var mixins = this.includes[thing.name]; if (mixins) { mixins = mixins.map(function(id) { var mixin = this.members[id]; if (!mixin) { throw new Error("Interface " + id + " not found (implemented by " + thing.name + ")"); } return mixin; }, this); if (mixins.some(function(m) { return m.has_to_json_regular_operation() } )) { return true; } } if (!thing.base) { return false; } base = this.members[thing.base]; if (!base) { throw new Error("Interface " + thing.base + " not found (inherited by " + thing.name + ")"); } thing = base; } return false; } return false; } }; function exposure_set(object, default_set) { var exposed = object.extAttrs && object.extAttrs.filter(a => a.name === "Exposed"); if (exposed && exposed.length > 1) { throw new IdlHarnessError( `Multiple 'Exposed' extended attributes on ${object.name}`); } let result = default_set || ["Window"]; if (result && !(result instanceof Set)) { result = new Set(result); } if (exposed && exposed.length) { const { rhs } = exposed[0]; // Could be a list or a string. const set = rhs.type === "*" ? [ "*" ] : rhs.type === "identifier-list" ? rhs.value.map(id => id.value) : [ rhs.value ]; result = new Set(set); } if (result && result.has("*")) { return "*"; } if (result && result.has("Worker")) { result.delete("Worker"); result.add("DedicatedWorker"); result.add("ServiceWorker"); result.add("SharedWorker"); } return result; } function exposed_in(globals) { if (globals === "*") { return true; } if ('Window' in self) { return globals.has("Window"); } if ('DedicatedWorkerGlobalScope' in self && self instanceof DedicatedWorkerGlobalScope) { return globals.has("DedicatedWorker"); } if ('SharedWorkerGlobalScope' in self && self instanceof SharedWorkerGlobalScope) { return globals.has("SharedWorker"); } if ('ServiceWorkerGlobalScope' in self && self instanceof ServiceWorkerGlobalScope) { return globals.has("ServiceWorker"); } if (Object.getPrototypeOf(self) === Object.prototype) { // ShadowRealm - only exposed with `"*"`. return false; } throw new IdlHarnessError("Unexpected global object"); } /** * Asserts that the given error message is thrown for the given function. * @param {string|IdlHarnessError} error Expected Error message. * @param {Function} idlArrayFunc Function operating on an IdlArray that should throw. */ IdlArray.prototype.assert_throws = function(error, idlArrayFunc) { try { idlArrayFunc.call(this, this); } catch (e) { if (e instanceof AssertionError) { throw e; } // Assertions for behaviour of the idlharness.js engine. if (error instanceof IdlHarnessError) { error = error.message; } if (e.message !== error) { throw new IdlHarnessError(`${idlArrayFunc} threw "${e}", not the expected IdlHarnessError "${error}"`); } return; } throw new IdlHarnessError(`${idlArrayFunc} did not throw the expected IdlHarnessError`); } IdlArray.prototype.test = function() { /** Entry point. See documentation at beginning of file. */ // First merge in all partial definitions and interface mixins. this.merge_partials(); this.merge_mixins(); // Assert B defined for A : B for (const member of Object.values(this.members).filter(m => m.base)) { const lhs = member.name; const rhs = member.base; if (!(rhs in this.members)) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is undefined.`); const lhs_is_interface = this.members[lhs] instanceof IdlInterface; const rhs_is_interface = this.members[rhs] instanceof IdlInterface; if (rhs_is_interface != lhs_is_interface) { if (!lhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${lhs} is not an interface.`); if (!rhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is not an interface.`); } // Check for circular dependencies. member.get_reverse_inheritance_stack(); } Object.getOwnPropertyNames(this.members).forEach(function(memberName) { var member = this.members[memberName]; if (!(member instanceof IdlInterface)) { return; } var globals = exposure_set(member); member.exposed = exposed_in(globals); member.exposureSet = globals; }.bind(this)); // Now run test() on every member, and test_object() for every object. for (var name in this.members) { this.members[name].test(); if (name in this.objects) { const objects = this.objects[name]; if (!objects || !Array.isArray(objects)) { throw new IdlHarnessError(`Invalid or empty objects for member ${name}`); } objects.forEach(function(str) { if (!this.members[name] || !(this.members[name] instanceof IdlInterface)) { throw new IdlHarnessError(`Invalid object member name ${name}`); } this.members[name].test_object(str); }.bind(this)); } } }; IdlArray.prototype.merge_partials = function() { const testedPartials = new Map(); this.partials.forEach(function(parsed_idl) { const originalExists = parsed_idl.name in this.members && (this.members[parsed_idl.name] instanceof IdlInterface || this.members[parsed_idl.name] instanceof IdlDictionary || this.members[parsed_idl.name] instanceof IdlNamespace); // Ensure unique test name in case of multiple partials. let partialTestName = parsed_idl.name; let partialTestCount = 1; if (testedPartials.has(parsed_idl.name)) { partialTestCount += testedPartials.get(parsed_idl.name); partialTestName = `${partialTestName}[${partialTestCount}]`; } testedPartials.set(parsed_idl.name, partialTestCount); if (!parsed_idl.untested) { test(function () { assert_true(originalExists, `Original ${parsed_idl.type} should be defined`); var expected; switch (parsed_idl.type) { case 'dictionary': expected = IdlDictionary; break; case 'namespace': expected = IdlNamespace; break; case 'interface': case 'interface mixin': default: expected = IdlInterface; break; } assert_true( expected.prototype.isPrototypeOf(this.members[parsed_idl.name]), `Original ${parsed_idl.name} definition should have type ${parsed_idl.type}`); }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: original ${parsed_idl.type} defined`); } if (!originalExists) { // Not good.. but keep calm and carry on. return; } if (parsed_idl.extAttrs) { // Special-case "Exposed". Must be a subset of original interface's exposure. // Exposed on a partial is the equivalent of having the same Exposed on all nested members. // See https://github.com/heycam/webidl/issues/154 for discrepency between Exposed and // other extended attributes on partial interfaces. const exposureAttr = parsed_idl.extAttrs.find(a => a.name === "Exposed"); if (exposureAttr) { if (!parsed_idl.untested) { test(function () { const partialExposure = exposure_set(parsed_idl); const memberExposure = exposure_set(this.members[parsed_idl.name]); if (memberExposure === "*") { return; } if (partialExposure === "*") { throw new IdlHarnessError( `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed everywhere, the original ${parsed_idl.type} is not.`); } partialExposure.forEach(name => { if (!memberExposure || !memberExposure.has(name)) { throw new IdlHarnessError( `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed to '${name}', the original ${parsed_idl.type} is not.`); } }); }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: valid exposure set`); } parsed_idl.members.forEach(function (member) { member.extAttrs.push(exposureAttr); }.bind(this)); } parsed_idl.extAttrs.forEach(function(extAttr) { // "Exposed" already handled above. if (extAttr.name === "Exposed") { return; } this.members[parsed_idl.name].extAttrs.push(extAttr); }.bind(this)); } if (parsed_idl.members.length) { test(function () { var clash = parsed_idl.members.find(function(member) { return this.members[parsed_idl.name].members.find(function(m) { return this.are_duplicate_members(m, member); }.bind(this)); }.bind(this)); parsed_idl.members.forEach(function(member) { this.members[parsed_idl.name].members.push(new IdlInterfaceMember(member)); }.bind(this)); assert_true(!clash, "member " + (clash && clash.name) + " is unique"); }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: member names are unique`); } }.bind(this)); this.partials = []; } IdlArray.prototype.merge_mixins = function() { for (const parsed_idl of this.includes) { const lhs = parsed_idl.target; const rhs = parsed_idl.includes; var errStr = lhs + " includes " + rhs + ", but "; if (!(lhs in this.members)) throw errStr + lhs + " is undefined."; if (!(this.members[lhs] instanceof IdlInterface)) throw errStr + lhs + " is not an interface."; if (!(rhs in this.members)) throw errStr + rhs + " is undefined."; if (!(this.members[rhs] instanceof IdlInterface)) throw errStr + rhs + " is not an interface."; if (this.members[rhs].members.length) { test(function () { var clash = this.members[rhs].members.find(function(member) { return this.members[lhs].members.find(function(m) { return this.are_duplicate_members(m, member); }.bind(this)); }.bind(this)); this.members[rhs].members.forEach(function(member) { assert_true( this.members[lhs].members.every(m => !this.are_duplicate_members(m, member)), "member " + member.name + " is unique"); this.members[lhs].members.push(new IdlInterfaceMember(member)); }.bind(this)); assert_true(!clash, "member " + (clash && clash.name) + " is unique"); }.bind(this), lhs + " includes " + rhs + ": member names are unique"); } } this.includes = []; } IdlArray.prototype.are_duplicate_members = function(m1, m2) { if (m1.name !== m2.name) { return false; } if (m1.type === 'operation' && m2.type === 'operation' && m1.arguments.length !== m2.arguments.length) { // Method overload. TODO: Deep comparison of arguments. return false; } return true; } IdlArray.prototype.assert_type_is = function(value, type) { if (type.idlType in this.members && this.members[type.idlType] instanceof IdlTypedef) { this.assert_type_is(value, this.members[type.idlType].idlType); return; } if (type.nullable && value === null) { // This is fine return; } if (type.union) { for (var i = 0; i < type.idlType.length; i++) { try { this.assert_type_is(value, type.idlType[i]); // No AssertionError, so we match one type in the union return; } catch(e) { if (e instanceof AssertionError) { // We didn't match this type, let's try some others continue; } throw e; } } // TODO: Is there a nice way to list the union's types in the message? assert_true(false, "Attribute has value " + format_value(value) + " which doesn't match any of the types in the union"); } /** * Helper function that tests that value is an instance of type according * to the rules of WebIDL. value is any JavaScript value, and type is an * object produced by WebIDLParser.js' "type" production. That production * is fairly elaborate due to the complexity of WebIDL's types, so it's * best to look at the grammar to figure out what properties it might have. */ if (type.idlType == "any") { // No assertions to make return; } if (type.array) { // TODO: not supported yet return; } if (type.generic === "sequence" || type.generic == "ObservableArray") { assert_true(Array.isArray(value), "should be an Array"); if (!value.length) { // Nothing we can do. return; } this.assert_type_is(value[0], type.idlType[0]); return; } if (type.generic === "Promise") { assert_true("then" in value, "Attribute with a Promise type should have a then property"); // TODO: Ideally, we would check on project fulfillment // that we get the right type // but that would require making the type check async return; } if (type.generic === "FrozenArray") { assert_true(Array.isArray(value), "Value should be array"); assert_true(Object.isFrozen(value), "Value should be frozen"); if (!value.length) { // Nothing we can do. return; } this.assert_type_is(value[0], type.idlType[0]); return; } type = Array.isArray(type.idlType) ? type.idlType[0] : type.idlType; switch(type) { case "undefined": assert_equals(value, undefined); return; case "boolean": assert_equals(typeof value, "boolean"); return; case "byte": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(-128 <= value && value <= 127, "byte " + value + " should be in range [-128, 127]"); return; case "octet": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(0 <= value && value <= 255, "octet " + value + " should be in range [0, 255]"); return; case "short": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(-32768 <= value && value <= 32767, "short " + value + " should be in range [-32768, 32767]"); return; case "unsigned short": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(0 <= value && value <= 65535, "unsigned short " + value + " should be in range [0, 65535]"); return; case "long": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(-2147483648 <= value && value <= 2147483647, "long " + value + " should be in range [-2147483648, 2147483647]"); return; case "unsigned long": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(0 <= value && value <= 4294967295, "unsigned long " + value + " should be in range [0, 4294967295]"); return; case "long long": assert_equals(typeof value, "number"); return; case "unsigned long long": case "DOMTimeStamp": assert_equals(typeof value, "number"); assert_true(0 <= value, "unsigned long long should be positive"); return; case "float": assert_equals(typeof value, "number"); assert_equals(value, Math.fround(value), "float rounded to 32-bit float should be itself"); assert_not_equals(value, Infinity); assert_not_equals(value, -Infinity); assert_not_equals(value, NaN); return; case "DOMHighResTimeStamp": case "double": assert_equals(typeof value, "number"); assert_not_equals(value, Infinity); assert_not_equals(value, -Infinity); assert_not_equals(value, NaN); return; case "unrestricted float": assert_equals(typeof value, "number"); assert_equals(value, Math.fround(value), "unrestricted float rounded to 32-bit float should be itself"); return; case "unrestricted double": assert_equals(typeof value, "number"); return; case "DOMString": assert_equals(typeof value, "string"); return; case "ByteString": assert_equals(typeof value, "string"); assert_regexp_match(value, /^[\x00-\x7F]*$/); return; case "USVString": assert_equals(typeof value, "string"); assert_regexp_match(value, /^([\x00-\ud7ff\ue000-\uffff]|[\ud800-\udbff][\udc00-\udfff])*$/); return; case "ArrayBufferView": assert_true(ArrayBuffer.isView(value)); return; case "object": assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function"); return; } // This is a catch-all for any IDL type name which follows JS class // semantics. This includes some non-interface IDL types (e.g. Int8Array, // Function, ...), as well as any interface types that are not in the IDL // that is fed to the harness. If an IDL type does not follow JS class // semantics then it should go in the switch statement above. If an IDL // type needs full checking, then the test should include it in the IDL it // feeds to the harness. if (!(type in this.members)) { assert_true(value instanceof self[type], "wrong type: not a " + type); return; } if (this.members[type] instanceof IdlInterface) { // We don't want to run the full // IdlInterface.prototype.test_instance_of, because that could result // in an infinite loop. TODO: This means we don't have tests for // LegacyNoInterfaceObject interfaces, and we also can't test objects // that come from another self. assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function"); if (value instanceof Object && !this.members[type].has_extended_attribute("LegacyNoInterfaceObject") && type in self) { assert_true(value instanceof self[type], "instanceof " + type); } } else if (this.members[type] instanceof IdlEnum) { assert_equals(typeof value, "string"); } else if (this.members[type] instanceof IdlDictionary) { // TODO: Test when we actually have something to test this on } else if (this.members[type] instanceof IdlCallback) { assert_equals(typeof value, "function"); } else { throw new IdlHarnessError("Type " + type + " isn't an interface, callback or dictionary"); } }; /// IdlObject /// function IdlObject() {} IdlObject.prototype.test = function() { /** * By default, this does nothing, so no actual tests are run for IdlObjects * that don't define any (e.g., IdlDictionary at the time of this writing). */ }; IdlObject.prototype.has_extended_attribute = function(name) { /** * This is only meaningful for things that support extended attributes, * such as interfaces, exceptions, and members. */ return this.extAttrs.some(function(o) { return o.name == name; }); }; /// IdlDictionary /// // Used for IdlArray.prototype.assert_type_is function IdlDictionary(obj) { /** * obj is an object produced by the WebIDLParser.js "dictionary" * production. */ /** Self-explanatory. */ this.name = obj.name; /** A back-reference to our IdlArray. */ this.array = obj.array; /** An array of objects produced by the "dictionaryMember" production. */ this.members = obj.members; /** * The name (as a string) of the dictionary type we inherit from, or null * if there is none. */ this.base = obj.inheritance; } IdlDictionary.prototype = Object.create(IdlObject.prototype); IdlDictionary.prototype.get_reverse_inheritance_stack = function() { return IdlInterface.prototype.get_reverse_inheritance_stack.call(this); }; /// IdlInterface /// function IdlInterface(obj, is_callback, is_mixin) { /** * obj is an object produced by the WebIDLParser.js "interface" production. */ /** Self-explanatory. */ this.name = obj.name; /** A back-reference to our IdlArray. */ this.array = obj.array; /** * An indicator of whether we should run tests on the interface object and * interface prototype object. Tests on members are controlled by .untested * on each member, not this. */ this.untested = obj.untested; /** An array of objects produced by the "ExtAttr" production. */ this.extAttrs = obj.extAttrs; /** An array of IdlInterfaceMembers. */ this.members = obj.members.map(function(m){return new IdlInterfaceMember(m); }); if (this.has_extended_attribute("LegacyUnforgeable")) { this.members .filter(function(m) { return m.special !== "static" && (m.type == "attribute" || m.type == "operation"); }) .forEach(function(m) { return m.isUnforgeable = true; }); } /** * The name (as a string) of the type we inherit from, or null if there is * none. */ this.base = obj.inheritance; this._is_callback = is_callback; this._is_mixin = is_mixin; } IdlInterface.prototype = Object.create(IdlObject.prototype); IdlInterface.prototype.is_callback = function() { return this._is_callback; }; IdlInterface.prototype.is_mixin = function() { return this._is_mixin; }; IdlInterface.prototype.has_constants = function() { return this.members.some(function(member) { return member.type === "const"; }); }; IdlInterface.prototype.get_unscopables = function() { return this.members.filter(function(member) { return member.isUnscopable; }); }; IdlInterface.prototype.is_global = function() { return this.extAttrs.some(function(attribute) { return attribute.name === "Global"; }); }; /** * Value of the LegacyNamespace extended attribute, if any. * * https://webidl.spec.whatwg.org/#LegacyNamespace */ IdlInterface.prototype.get_legacy_namespace = function() { var legacyNamespace = this.extAttrs.find(function(attribute) { return attribute.name === "LegacyNamespace"; }); return legacyNamespace ? legacyNamespace.rhs.value : undefined; }; IdlInterface.prototype.get_interface_object_owner = function() { var legacyNamespace = this.get_legacy_namespace(); return legacyNamespace ? self[legacyNamespace] : self; }; IdlInterface.prototype.should_have_interface_object = function() { // "For every interface that is exposed in a given ECMAScript global // environment and: // * is a callback interface that has constants declared on it, or // * is a non-callback interface that is not declared with the // [LegacyNoInterfaceObject] extended attribute, // a corresponding property MUST exist on the ECMAScript global object. return this.is_callback() ? this.has_constants() : !this.has_extended_attribute("LegacyNoInterfaceObject"); }; IdlInterface.prototype.assert_interface_object_exists = function() { var owner = this.get_legacy_namespace() || "self"; assert_own_property(self[owner], this.name, owner + " does not have own property " + format_value(this.name)); }; IdlInterface.prototype.get_interface_object = function() { if (!this.should_have_interface_object()) { var reason = this.is_callback() ? "lack of declared constants" : "declared [LegacyNoInterfaceObject] attribute"; throw new IdlHarnessError(this.name + " has no interface object due to " + reason); } return this.get_interface_object_owner()[this.name]; }; IdlInterface.prototype.get_qualified_name = function() { // https://webidl.spec.whatwg.org/#qualified-name var legacyNamespace = this.get_legacy_namespace(); if (legacyNamespace) { return legacyNamespace + "." + this.name; } return this.name; }; IdlInterface.prototype.has_to_json_regular_operation = function() { return this.members.some(function(m) { return m.is_to_json_regular_operation(); }); }; IdlInterface.prototype.has_default_to_json_regular_operation = function() { return this.members.some(function(m) { return m.is_to_json_regular_operation() && m.has_extended_attribute("Default"); }); }; /** * Implementation of https://webidl.spec.whatwg.org/#create-an-inheritance-stack * with the order reversed. * * The order is reversed so that the base class comes first in the list, because * this is what all call sites need. * * So given: * * A : B {}; * B : C {}; * C {}; * * then A.get_reverse_inheritance_stack() returns [C, B, A], * and B.get_reverse_inheritance_stack() returns [C, B]. * * Note: as dictionary inheritance is expressed identically by the AST, * this works just as well for getting a stack of inherited dictionaries. */ IdlInterface.prototype.get_reverse_inheritance_stack = function() { const stack = [this]; let idl_interface = this; while (idl_interface.base) { const base = this.array.members[idl_interface.base]; if (!base) { throw new Error(idl_interface.type + " " + idl_interface.base + " not found (inherited by " + idl_interface.name + ")"); } else if (stack.indexOf(base) > -1) { stack.unshift(base); const dep_chain = stack.map(i => i.name).join(','); throw new IdlHarnessError(`${this.name} has a circular dependency: ${dep_chain}`); } idl_interface = base; stack.unshift(idl_interface); } return stack; }; /** * Implementation of * https://webidl.spec.whatwg.org/#default-tojson-operation * for testing purposes. * * Collects the IDL types of the attributes that meet the criteria * for inclusion in the default toJSON operation for easy * comparison with actual value */ IdlInterface.prototype.default_to_json_operation = function() { const map = new Map() let isDefault = false; for (const I of this.get_reverse_inheritance_stack()) { if (I.has_default_to_json_regular_operation()) { isDefault = true; for (const m of I.members) { if (m.special !== "static" && m.type == "attribute" && I.array.is_json_type(m.idlType)) { map.set(m.name, m.idlType); } } } else if (I.has_to_json_regular_operation()) { isDefault = false; } } return isDefault ? map : null; }; IdlInterface.prototype.test = function() { if (this.has_extended_attribute("LegacyNoInterfaceObject") || this.is_mixin()) { // No tests to do without an instance. TODO: We should still be able // to run tests on the prototype object, if we obtain one through some // other means. return; } // If the interface object is not exposed, only test that. Members can't be // tested either, but objects could still be tested in |test_object|. if (!this.exposed) { if (!this.untested) { subsetTestByKey(this.name, test, function() { assert_false(this.name in self); }.bind(this), this.name + " interface: existence and properties of interface object"); } return; } if (!this.untested) { // First test things to do with th