wpt-runner
Version:
Runs web platform tests in Node.js using jsdom
1,431 lines (1,280 loc) • 145 kB
JavaScript
/* 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