Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions test/common/wpt/rejections.js
Original file line number Diff line number Diff line change
@@ -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);
};
};
2 changes: 2 additions & 0 deletions test/common/wpt/webworker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -88,6 +89,7 @@ globalThis.onmessage = ({ data }) => {
const result = realImportScripts.apply(this, mapped);
if (mapped.includes(testharnessPath)) {
applySkips();
honorAllowedRejections();
}
return result;
};
Expand Down
2 changes: 2 additions & 0 deletions test/common/wpt/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
} = require('vm');
const { setFlagsFromString } = require('v8');
const { inspect } = require('util');
const honorAllowedRejections = require('./rejections');
const {
isMainThread,
parentPort,
Expand Down Expand Up @@ -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
Expand Down
189 changes: 189 additions & 0 deletions test/parallel/test-common-wpt-allowed-rejections.js
Original file line number Diff line number Diff line change
@@ -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());
21 changes: 12 additions & 9 deletions test/wpt/status/FileAPI/blob.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
],
},
},
},
} : {}),
};
3 changes: 0 additions & 3 deletions test/wpt/status/wasm/webapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
8 changes: 0 additions & 8 deletions test/wpt/status/web-locks.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
Loading