diff --git a/test/common/wpt/rejections.js b/test/common/wpt/rejections.js new file mode 100644 index 000000000000..f0cda7a4c603 --- /dev/null +++ b/test/common/wpt/rejections.js @@ -0,0 +1,65 @@ +'use strict'; + +// Node.js does not dispatch the global unhandledrejection event used by +// testharness.js. Ignore rejections only when the harness settings allow them. +module.exports = function honorAllowedRejections() { + const { setup, promise_setup, add_result_callback, add_completion_callback } = globalThis; + let configurable = true; + let allowed = false; + let singleTest = false; + const ignoreRejection = () => {}; + + // The harness ignores configuration changes once results have started. + add_result_callback(() => { configurable = false; }); + add_completion_callback(() => { configurable = false; }); + + function apply(properties) { + if (properties) { + if (Object.prototype.propertyIsEnumerable.call(properties, 'allow_uncaught_exception')) { + allowed = properties.allow_uncaught_exception; + } + if (Object.prototype.propertyIsEnumerable.call(properties, 'single_test') && properties.single_test) { + singleTest = true; + } + } + process.removeListener('unhandledRejection', ignoreRejection); + // In single_test mode the harness still fails the implicit test when + // uncaught errors are allowed. Retain Node's failure path for that mode. + if (allowed && !singleTest) { + process.on('unhandledRejection', ignoreRejection); + } + } + + globalThis.setup = function(funcOrProperties, maybeProperties) { + let func; + let properties; + if (arguments.length === 2) { + func = funcOrProperties; + properties = maybeProperties; + } else if (funcOrProperties instanceof Function) { + func = funcOrProperties; + properties = {}; + } else { + properties = funcOrProperties; + } + + // This callback runs only when setup was accepted, before any nested + // setup calls made by the test's callback can override its settings. + return setup.call(this, () => { + apply(properties); + if (func) func(); + }, properties); + }; + + globalThis.promise_setup = function(func, properties) { + if (typeof func !== 'function') { + return promise_setup.call(this, func, properties); + } + return promise_setup.call(this, () => { + // promise_setup applies settings when its queued callback executes. + // Unlike setup, it still invokes that callback after results start. + if (configurable) apply(properties); + return func(); + }, properties); + }; +}; diff --git a/test/common/wpt/webworker.js b/test/common/wpt/webworker.js index e9a8ebe53d79..8a335c808692 100644 --- a/test/common/wpt/webworker.js +++ b/test/common/wpt/webworker.js @@ -4,6 +4,7 @@ // Refs: https://web-platform-tests.org/writing-tests/testharness.html const { pathToFileURL } = require('url'); +const honorAllowedRejections = require('./rejections'); const { runInThisContext, constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER }, @@ -88,6 +89,7 @@ globalThis.onmessage = ({ data }) => { const result = realImportScripts.apply(this, mapped); if (mapped.includes(testharnessPath)) { applySkips(); + honorAllowedRejections(); } return result; }; diff --git a/test/common/wpt/worker.js b/test/common/wpt/worker.js index ca4f4706f89b..7d84f2ac1a36 100644 --- a/test/common/wpt/worker.js +++ b/test/common/wpt/worker.js @@ -9,6 +9,7 @@ const { } = require('vm'); const { setFlagsFromString } = require('v8'); const { inspect } = require('util'); +const honorAllowedRejections = require('./rejections'); const { isMainThread, parentPort, @@ -79,6 +80,7 @@ function run(workerData) { filename: workerData.harness.filename, importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER, }); + honorAllowedRejections(); // If there are skip patterns, wrap test functions to prevent execution of // matching tests. This must happen after testharness.js is loaded but before diff --git a/test/parallel/test-common-wpt-allowed-rejections.js b/test/parallel/test-common-wpt-allowed-rejections.js new file mode 100644 index 000000000000..e8f8ed113f3f --- /dev/null +++ b/test/parallel/test-common-wpt-allowed-rejections.js @@ -0,0 +1,189 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { backends } = require('../common/wpt'); + +const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js'); +const harness = { + code: fs.readFileSync(harnessPath, 'utf8'), + filename: harnessPath, +}; + +async function check(backend, webWorker, { + name, + code, + allowed, + passes = 1, + singleTest = false, + message = /uncaught rejection probe/, +}) { + const label = `${backend}, ${webWorker}, ${name}`; + const script = { + filename: fixtures.path('wpt-rejection-probe.js'), + // Use a direct worker script for single_test: .any.js wrappers call + // done() automatically, completing the implicit test before its timers. + code: `${webWorker && singleTest ? 'importScripts("/resources/testharness.js");' : ''}\n${code}`, + }; + const workerData = { + testRelativePath: 'rejection-probe.any.js', + wptRunner: path.join(__dirname, '../common/wpt.js'), + wptPath: 'compression', + harness, + scriptsToRun: webWorker ? [] : [script], + webWorker: webWorker ? { + path: script.filename, + modifiedScript: script, + isAnyTest: !singleTest, + variant: '', + scripts: [], + } : undefined, + }; + + let result; + const statuses = []; + const handle = backends[backend](['--experimental-web-worker'], workerData, { + message(message) { + if (message.type === 'result') { + statuses.push(message.result.status); + } else if (message.type === 'completion') { + // The process backend can complete before its uncaught-error handler + // exits. Preserve any failure it already reported. + result ||= message.status; + handle.kill(); + } else { + assert.fail(`Unexpected message type: ${message.type}`); + } + }, + failure(error) { + if (result) return false; + result = { status: 1, message: error.message }; + return true; + }, + }); + const timeout = setTimeout(() => { + handle.kill(); + assert.fail(`WPT rejection probe did not finish: ${label}`); + }, common.platformTimeout(10_000)); + + try { + await handle.finished; + } finally { + clearTimeout(timeout); + } + + assert(result, label); + assert.strictEqual(result.status, allowed ? 0 : 1, label); + if (allowed) { + assert.deepStrictEqual(statuses, Array(passes).fill(0), label); + } else { + assert.match(result.message, message, label); + } +} + +(async () => { + for (const backend of ['thread', 'process']) { + for (const webWorker of [false, true]) { + for (const [setup, allowed] of [ + ['', false], + ['setup({ allow_uncaught_exception: false });', false], + ['setup({ allow_uncaught_exception: true });', true], + ['setup(() => {}, { allow_uncaught_exception: true });', true], + ['setup(null, { allow_uncaught_exception: true });', true], + ['setup({ allow_uncaught_exception: true }); setup({});', true], + ['setup({ allow_uncaught_exception: true }); setup({ allow_uncaught_exception: false });', false], + ['setup(Object.defineProperty({}, "allow_uncaught_exception", { value: true }));', false], + ]) { + await check(backend, webWorker, { + name: setup || 'default settings', + code: `${setup} + const t = async_test('rejection probe'); + Promise.reject(new Error('uncaught rejection probe')); + setTimeout(() => t.done(), 0);`, + allowed, + }); + } + + for (const allowed of [false, true]) { + await check(backend, webWorker, { + name: `nested setup allows rejection: ${allowed}`, + code: `setup(() => setup({ allow_uncaught_exception: ${allowed} }), + { allow_uncaught_exception: ${!allowed} }); + const t = async_test('rejection probe'); + Promise.reject(new Error('uncaught rejection probe')); + setTimeout(() => t.done(), 0);`, + allowed, + }); + + await check(backend, webWorker, { + name: `late setup preserves allowance: ${allowed}`, + code: `setup({ allow_uncaught_exception: ${allowed} }); + const t = async_test('rejection probe'); + test(() => {}, 'first result'); + setup(() => assert_unreached('late setup callback'), + { allow_uncaught_exception: ${!allowed} }); + Promise.reject(new Error('uncaught rejection probe')); + setTimeout(() => t.done(), 0);`, + allowed, + passes: 2, + }); + + await check(backend, webWorker, { + name: `promise_setup applies deferred allowance: ${allowed}`, + code: `promise_setup(async () => {}, { allow_uncaught_exception: ${allowed} }); + setup({ allow_uncaught_exception: ${!allowed} }); + promise_test(async () => { + Promise.reject(new Error('uncaught rejection probe')); + await new Promise(resolve => setTimeout(resolve, 0)); + }, 'rejection probe');`, + allowed, + }); + + await check(backend, webWorker, { + name: `late promise_setup preserves allowance: ${allowed}`, + code: `setup({ allow_uncaught_exception: ${allowed} }); + const t = async_test('rejection probe'); + test(() => {}, 'first result'); + promise_setup(async () => { + Promise.reject(new Error('uncaught rejection probe')); + setTimeout(() => t.done(), 0); + }, { allow_uncaught_exception: ${!allowed} });`, + allowed, + passes: 2, + }); + + await check(backend, webWorker, { + name: `single_test still fails with allowance: ${allowed}`, + code: `setup({ single_test: true, allow_uncaught_exception: ${allowed} }); + setup({ single_test: false }); + Promise.reject(new Error('uncaught rejection probe')); + setTimeout(done, 0);`, + allowed: false, + singleTest: true, + }); + } + + for (const setup of [ + 'setup(() => { throw new Error("setup failure"); }, { allow_uncaught_exception: true });', + 'promise_setup(async () => { throw new Error("setup failure"); }, { allow_uncaught_exception: true });', + ]) { + await check(backend, webWorker, { + name: setup, + code: `${setup}\npromise_test(async () => {}, 'setup must succeed first');`, + allowed: false, + message: /setup failure/, + }); + } + + await check(backend, webWorker, { + name: 'unexpected rejection fails even with a permanently pending test', + code: `async_test('pending forever'); + Promise.reject(new Error('uncaught rejection probe'));`, + allowed: false, + }); + } + } +})().then(common.mustCall()); diff --git a/test/wpt/status/FileAPI/blob.cjs b/test/wpt/status/FileAPI/blob.cjs index 6f8599dc8b4e..c59516ca1dc5 100644 --- a/test/wpt/status/FileAPI/blob.cjs +++ b/test/wpt/status/FileAPI/blob.cjs @@ -23,14 +23,17 @@ module.exports = { 'Blob-constructor-dom.window.js': { skip: 'Depends on DOM API', }, - 'Blob-constructor.any.js': { - fail: { - flaky: [ - 'Passing typed arrays as elements of the blobParts array should work.', - 'Passing a Float16Array as element of the blobParts array should work.', - 'Passing a Float64Array as element of the blobParts array should work.', - 'Passing BigInt typed arrays as elements of the blobParts array should work.', - ], + ...(os.endianness() === 'BE' ? { + 'Blob-constructor.any.js': { + fail: { + note: 'The expected bytes assume a little-endian platform', + expected: [ + 'Passing typed arrays as elements of the blobParts array should work.', + 'Passing a Float16Array as element of the blobParts array should work.', + 'Passing a Float64Array as element of the blobParts array should work.', + 'Passing BigInt typed arrays as elements of the blobParts array should work.', + ], + }, }, - }, + } : {}), }; diff --git a/test/wpt/status/wasm/webapi.json b/test/wpt/status/wasm/webapi.json index 3e2075655e5a..6328e55dc18b 100644 --- a/test/wpt/status/wasm/webapi.json +++ b/test/wpt/status/wasm/webapi.json @@ -16,8 +16,5 @@ }, "status.any.js": { "skip": "WPTRunner does not support fetch()" - }, - "instantiateStreaming-bad-imports.any.js": { - "skip": "Flaky on ARM with V8 >= 11.2" } } diff --git a/test/wpt/status/web-locks.json b/test/wpt/status/web-locks.json index 97a0a7900d64..f7774a38fa76 100644 --- a/test/wpt/status/web-locks.json +++ b/test/wpt/status/web-locks.json @@ -1,12 +1,4 @@ { - "held.https.any.js": { - "fail": { - "note": "Flaky on ppc, linux x64 and s390x #59142", - "flaky": [ - "Error: this uncaught rejection is expected" - ] - } - }, "idlharness.https.any.js": { "fail": { "expected": [