mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
test: honor allowed WPT promise rejections
Honor allow_uncaught_exception for unhandled promise rejections in both WPT worker paths. Apply setup settings before nested callbacks, preserve settings after results start, and apply promise_setup settings when its callback runs. Keep rejection failures in single_test mode. Cover both runner backends and Web Workers, and remove the flaky expectation for the Web Locks test that deliberately rejects. Refs: https://github.com/nodejs/node/issues/59142 Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Claude, Codex PR-URL: https://github.com/nodejs/node/pull/66322 Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
This commit is contained in:
1 parent
3f2450f630
commit
0ded159ee1
5 files changed
+258
-8
No files matched your search
@@ -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);
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
@@ -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": [
|
||||
|
||||
Reference in new issue
Block a user