From 694e3c3db69be75aa76a798acb7169af3cdc7ea8 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 25 Nov 2024 10:19:10 -0500 Subject: [PATCH] improve wpt harness documentations --- src/workerd/api/wpt/url-test.js | 2 +- src/workerd/api/wpt/urlpattern-test.js | 4 +- src/wpt/harness.js | 279 ++++++++++++++----------- 3 files changed, 161 insertions(+), 124 deletions(-) diff --git a/src/workerd/api/wpt/url-test.js b/src/workerd/api/wpt/url-test.js index f3af42faa9a..6c7f9edf615 100644 --- a/src/workerd/api/wpt/url-test.js +++ b/src/workerd/api/wpt/url-test.js @@ -129,7 +129,7 @@ export const urlSearchParamsConstructor = run( ); export const urlSearchParamsDelete = run('urlsearchparams-delete.any.js'); export const urlSearchParamsForEach = run('urlsearchparams-foreach.any.js', { - skippedTests: ['For-of Check'], + expectedFailures: ['For-of Check'], }); export const urlSearchParamsGetAny = run('urlsearchparams-get.any.js'); export const urlSearchParamsGetAll = run('urlsearchparams-getall.any.js'); diff --git a/src/workerd/api/wpt/urlpattern-test.js b/src/workerd/api/wpt/urlpattern-test.js index f399c8a1ba0..b20be359d41 100644 --- a/src/workerd/api/wpt/urlpattern-test.js +++ b/src/workerd/api/wpt/urlpattern-test.js @@ -4,7 +4,7 @@ import { run } from 'harness'; -export const urlpatternCompare = run('urlpattern-compare-tests.js', { +export const urlpatternCompareTests = run('urlpattern-compare-tests.js', { expectedFailures: [ // Each of these *ought* to pass. They are included here because we // know they currently do not. Each needs to be investigated. @@ -45,7 +45,7 @@ export const urlpatternHasRegexGroups = run( ], } ); -export const urlpattern = run('urlpatterntests.js', { +export const urlPatternTests = run('urlpatterntests.js', { expectedFailures: [ // Each of these *ought* to pass. They are included here because we // know they currently do not. Each needs to be investigated. diff --git a/src/wpt/harness.js b/src/wpt/harness.js index 48bffed11e6..6e6e8256a7e 100644 --- a/src/wpt/harness.js +++ b/src/wpt/harness.js @@ -24,17 +24,14 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. import { + AssertionError, strictEqual, notStrictEqual, deepStrictEqual, - throws, ok, + throws, } from 'node:assert'; -globalThis.AssertionError = function AssertionError(message) { - this.message = message; -}; - function OptionalFeatureUnsupportedError(message) { AssertionError.call(this, message); } @@ -42,6 +39,8 @@ OptionalFeatureUnsupportedError.prototype = Object.create( AssertionError.prototype ); +globalThis.Window = Object.getPrototypeOf(globalThis).constructor; + globalThis.fetch = async (url) => { const { default: data } = await import(url); return { @@ -63,17 +62,21 @@ globalThis.subsetTestByKey = (_key, testType, testCallback, testMessage) => { return testType(testCallback, testMessage); }; -globalThis.promise_test = (callback, message) => { - if (!shouldRunTest(message)) { +globalThis.promise_test = async (func, name, properties) => { + if (typeof func !== 'function') { + properties = name; + name = func; + func = null; + } + + if (!shouldRunTest(name)) { return; } try { - globalThis.promises.push(callback.call(this)); + await func.call(this); } catch (err) { - const agg_err = new AggregateError([err], message); - agg_err.stack = err.stack; - globalThis.errors.push(agg_err); + globalThis.errors.push(new AggregateError([err], name)); } }; @@ -101,136 +104,165 @@ globalThis.assert_object_equals = (a, b, message) => { deepStrictEqual(a, b, message); }; -globalThis.assert_implements = (condition, message) => { - ok(!!condition, message); +/** + * Assert that a feature is implemented, based on a 'truthy' condition. + * + * This function should be used to early-exit from tests in which there is + * no point continuing without support for a non-optional spec or spec + * feature. For example: + * + * assert_implements(window.Foo, 'Foo is not supported'); + * + * @param {object} condition The truthy value to test + * @param {string} [description] Error description for the case that the condition is not truthy. + */ +globalThis.assert_implements = (condition, description) => { + ok(!!condition, description); }; +/** + * Assert that an optional feature is implemented, based on a 'truthy' condition. + * + * This function should be used to early-exit from tests in which there is + * no point continuing without support for an explicitly optional spec or + * spec feature. For example: + * + * assert_implements_optional(video.canPlayType("video/webm"), + * "webm video playback not supported"); + * + * @param {object} condition The truthy value to test + * @param {string} [description] Error description for the case that the condition is not truthy. + */ globalThis.assert_implements_optional = (condition, description) => { if (!condition) { throw new OptionalFeatureUnsupportedError(description); } }; -// Implementation is taken from Node.js WPT harness. -// Ref: https://github.com/nodejs/node/blob/8807549ed9f6eaf6842ae56b8ac55ab385951636/test/fixtures/wpt/resources/testharness.js#L2046 -globalThis.assert_throws_js = ( - constructor, - func, - description, - assertion_type -) => { - try { - func.call(this); - ok(false, assertion_type, description, '${func} did not throw', { - func: func, - }); - } catch (e) { - if (e instanceof AssertionError) { - throw e; - } - - // Basic sanity-checks on the thrown exception. - ok( - typeof e === 'object', - assertion_type, - description, - '${func} threw ${e} with type ${type}, not an object', - { func: func, e: e, type: typeof e } - ); - - ok( - e !== null, - assertion_type, - description, - '${func} threw null, not an object', - { func: func } - ); - - // Basic sanity-check on the passed-in constructor - ok( - typeof constructor == 'function', - assertion_type, - description, - '${constructor} is not a constructor', - { constructor: constructor } - ); - var obj = constructor; - while (obj) { - if (typeof obj === 'function' && obj.name === 'Error') { - break; - } - obj = Object.getPrototypeOf(obj); - } - ok( - obj != null, - assertion_type, - description, - '${constructor} is not an Error subtype', - { constructor: constructor } - ); +/** + * Asserts if called. Used to ensure that a specific code path is + * not taken e.g. that an error event isn't fired. + * + * @param {string} [description] - Description of the condition being tested. + */ +globalThis.assert_unreached = (description) => { + ok(false, `Reached unreachable code: ${description}`); +}; - // And checking that our exception is reasonable - ok( - e.constructor === constructor && e.name === constructor.name, - assertion_type, - description, - '${func} threw ${actual} (${actual_name}) expected instance of ${expected} (${expected_name})', - { - func: func, - actual: e, - actual_name: e.name, - expected: constructor, - expected_name: constructor.name, - } - ); - } +/** + * Assert a JS Error with the expected constructor is thrown. + * + * @param {object} constructor The expected exception constructor. + * @param {Function} func Function which should throw. + * @param {string} [description] Error description for the case that the error is not thrown. + */ +globalThis.assert_throws_js = (constructor, func, description) => { + throws( + () => { + func.call(this); + }, + constructor, + description + ); }; -globalThis.assert_throws_exactly = (expected, fn, message, assertion_type) => { - try { - fn.call(this); - ok(false, assertion_type, message, `${fn} did not throw`); - } catch (actual) { - if (e instanceof AssertionError) { - throw e; - } - deepStrictEqual(actual, expected, message); - } +/** + * Assert the provided value is thrown. + * + * @param {value} exception The expected exception. + * @param {Function} fn Function which should throw. + * @param {string} [description] Error description for the case that the error is not thrown. + */ +globalThis.assert_throws_exactly = (exception, fn, description) => { + throws( + () => { + fn.call(this); + }, + exception, + description + ); }; +/** + * Assert a DOMException with the expected type is thrown. + * + * There are two ways of calling assert_throws_dom: + * + * 1) If the DOMException is expected to come from the current global, the + * second argument should be the function expected to throw and a third, + * optional, argument is the assertion description. + * + * 2) If the DOMException is expected to come from some other global, the + * second argument should be the DOMException constructor from that global, + * the third argument the function expected to throw, and the fourth, optional, + * argument the assertion description. + * + * @param {number|string} type - The expected exception name or + * code. See the `table of names and codes + * `_. If a + * number is passed it should be one of the numeric code values in + * that table (e.g. 3, 4, etc). If a string is passed it can + * either be an exception name (e.g. "HierarchyRequestError", + * "WrongDocumentError") or the name of the corresponding error + * code (e.g. "``HIERARCHY_REQUEST_ERR``", "``WRONG_DOCUMENT_ERR``"). + * @param {Function} descriptionOrFunc - The function expected to + * throw (if the exception comes from another global), or the + * optional description of the condition being tested (if the + * exception comes from the current global). + * @param {string} [maybeDescription] - Description of the condition + * being tested (if the exception comes from another global). + * + */ globalThis.assert_throws_dom = ( type, - func, - description, - assertion_type, - constructor + funcOrConstructor, + descriptionOrFunc, + maybeDescription ) => { - try { - fn.call(this); - ok(false, assertion_type, description, `${func} did not throw`); - } catch (e) { - if (e instanceof AssertionError) { - throw e; - } - - strictEqual( - typeof e, - 'object', - `${func} threw with type ${typeof e}, not an object` + let constructor, func, description; + if (funcOrConstructor.name === 'DOMException') { + constructor = funcOrConstructor; + func = descriptionOrFunc; + description = maybeDescription; + } else { + constructor = this.DOMException; + func = funcOrConstructor; + description = descriptionOrFunc; + ok( + maybeDescription === undefined, + 'Too many args passed to no-constructor version of assert_throws_dom' ); - strictEqual(e, null, `${func} threw null, not an object`); } + + throws( + () => { + func.call(this); + }, + constructor, + description + ); }; -globalThis.test = (fn, message) => { - if (!shouldRunTest(message)) { +/** + * Create a synchronous test + * + * @param {TestFunction} func - Test function. This is executed + * immediately. If it returns without error, the test status is + * set to ``PASS``. If it throws an :js:class:`AssertionError`, or + * any other exception, the test status is set to ``FAIL`` + * (typically from an `assert` function). + * @param {String} name - Test name. This must be unique in a + * given file and must be invariant between runs. + */ +globalThis.test = (func, name) => { + if (!shouldRunTest(name)) { return; } try { - fn.call(this); + func.call(this); } catch (err) { - globalThis.errors.push(new AggregateError([err], message)); + globalThis.errors.push(new AggregateError([err], name)); } }; @@ -250,7 +282,6 @@ function shouldRunTest(message) { function prepare(options) { globalThis.errors = []; - globalThis.promises = []; globalThis.testOptions = options; } @@ -260,14 +291,19 @@ function sanitizeMessage(message) { // FFFE, and FFFF". // See https://www.w3.org/TR/REC-xml/#NT-Char return message.replace( - /[\u{D800}-\u{DFFF}\u{FFFE}\u{FFFF}]/gu, - (char) => '\\u' + char.charCodeAt().toString(16).padStart(4, '0') + /([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g, + function (_, low, prefix, high) { + let output = prefix || ''; // prefix may be undefined + const string = low || high; // only one of these alternates can match + for (let i = 0; i < string.length; i++) { + output += 'U+' + string[i].charCodeAt(0).toString(16); + } + return output; + } ); } async function validate(options) { - await Promise.all(globalThis.promises); - const expectedFailures = new Set(options.expectedFailures ?? []); for (const err of globalThis.errors) { @@ -278,6 +314,7 @@ async function validate(options) { } if (expectedFailures.size > 0) { + console.info('Expected failures', expectedFailures); throw new Error( 'Expected failures but test succeeded. Please update the test config file.' );