Skip to content

Commit 7d4fda6

Browse files
panvarichardlau
authored andcommitted
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: #59142 Signed-off-by: Filip Skokan <[email protected]> Assisted-by: Claude, Codex PR-URL: #66322 Reviewed-By: Daeyeon Jeong <[email protected]>
1 parent d4c87eb commit 7d4fda6

5 files changed

Lines changed: 258 additions & 8 deletions

File tree

‎test/common/wpt/rejections.js‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict';
2+
3+
// Node.js does not dispatch the global unhandledrejection event used by
4+
// testharness.js. Ignore rejections only when the harness settings allow them.
5+
module.exports = function honorAllowedRejections() {
6+
const { setup, promise_setup, add_result_callback, add_completion_callback } = globalThis;
7+
let configurable = true;
8+
let allowed = false;
9+
let singleTest = false;
10+
const ignoreRejection = () => {};
11+
12+
// The harness ignores configuration changes once results have started.
13+
add_result_callback(() => { configurable = false; });
14+
add_completion_callback(() => { configurable = false; });
15+
16+
function apply(properties) {
17+
if (properties) {
18+
if (Object.prototype.propertyIsEnumerable.call(properties, 'allow_uncaught_exception')) {
19+
allowed = properties.allow_uncaught_exception;
20+
}
21+
if (Object.prototype.propertyIsEnumerable.call(properties, 'single_test') && properties.single_test) {
22+
singleTest = true;
23+
}
24+
}
25+
process.removeListener('unhandledRejection', ignoreRejection);
26+
// In single_test mode the harness still fails the implicit test when
27+
// uncaught errors are allowed. Retain Node's failure path for that mode.
28+
if (allowed && !singleTest) {
29+
process.on('unhandledRejection', ignoreRejection);
30+
}
31+
}
32+
33+
globalThis.setup = function(funcOrProperties, maybeProperties) {
34+
let func;
35+
let properties;
36+
if (arguments.length === 2) {
37+
func = funcOrProperties;
38+
properties = maybeProperties;
39+
} else if (funcOrProperties instanceof Function) {
40+
func = funcOrProperties;
41+
properties = {};
42+
} else {
43+
properties = funcOrProperties;
44+
}
45+
46+
// This callback runs only when setup was accepted, before any nested
47+
// setup calls made by the test's callback can override its settings.
48+
return setup.call(this, () => {
49+
apply(properties);
50+
if (func) func();
51+
}, properties);
52+
};
53+
54+
globalThis.promise_setup = function(func, properties) {
55+
if (typeof func !== 'function') {
56+
return promise_setup.call(this, func, properties);
57+
}
58+
return promise_setup.call(this, () => {
59+
// promise_setup applies settings when its queued callback executes.
60+
// Unlike setup, it still invokes that callback after results start.
61+
if (configurable) apply(properties);
62+
return func();
63+
}, properties);
64+
};
65+
};

‎test/common/wpt/webworker.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// Refs: https://web-platform-tests.org/writing-tests/testharness.html
55

66
const { pathToFileURL } = require('url');
7+
const honorAllowedRejections = require('./rejections');
78
const {
89
runInThisContext,
910
constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER },
@@ -88,6 +89,7 @@ globalThis.onmessage = ({ data }) => {
8889
const result = realImportScripts.apply(this, mapped);
8990
if (mapped.includes(testharnessPath)) {
9091
applySkips();
92+
honorAllowedRejections();
9193
}
9294
return result;
9395
};

‎test/common/wpt/worker.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
} = require('vm');
1010
const { setFlagsFromString } = require('v8');
1111
const { inspect } = require('util');
12+
const honorAllowedRejections = require('./rejections');
1213
const {
1314
isMainThread,
1415
parentPort,
@@ -79,6 +80,7 @@ function run(workerData) {
7980
filename: workerData.harness.filename,
8081
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
8182
});
83+
honorAllowedRejections();
8284

8385
// If there are skip patterns, wrap test functions to prevent execution of
8486
// matching tests. This must happen after testharness.js is loaded but before
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const fixtures = require('../common/fixtures');
5+
const assert = require('assert');
6+
const fs = require('fs');
7+
const path = require('path');
8+
const { backends } = require('../common/wpt');
9+
10+
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
11+
const harness = {
12+
code: fs.readFileSync(harnessPath, 'utf8'),
13+
filename: harnessPath,
14+
};
15+
16+
async function check(backend, webWorker, {
17+
name,
18+
code,
19+
allowed,
20+
passes = 1,
21+
singleTest = false,
22+
message = /uncaught rejection probe/,
23+
}) {
24+
const label = `${backend}, ${webWorker}, ${name}`;
25+
const script = {
26+
filename: fixtures.path('wpt-rejection-probe.js'),
27+
// Use a direct worker script for single_test: .any.js wrappers call
28+
// done() automatically, completing the implicit test before its timers.
29+
code: `${webWorker && singleTest ? 'importScripts("/resources/testharness.js");' : ''}\n${code}`,
30+
};
31+
const workerData = {
32+
testRelativePath: 'rejection-probe.any.js',
33+
wptRunner: path.join(__dirname, '../common/wpt.js'),
34+
wptPath: 'compression',
35+
harness,
36+
scriptsToRun: webWorker ? [] : [script],
37+
webWorker: webWorker ? {
38+
path: script.filename,
39+
modifiedScript: script,
40+
isAnyTest: !singleTest,
41+
variant: '',
42+
scripts: [],
43+
} : undefined,
44+
};
45+
46+
let result;
47+
const statuses = [];
48+
const handle = backends[backend](['--experimental-web-worker'], workerData, {
49+
message(message) {
50+
if (message.type === 'result') {
51+
statuses.push(message.result.status);
52+
} else if (message.type === 'completion') {
53+
// The process backend can complete before its uncaught-error handler
54+
// exits. Preserve any failure it already reported.
55+
result ||= message.status;
56+
handle.kill();
57+
} else {
58+
assert.fail(`Unexpected message type: ${message.type}`);
59+
}
60+
},
61+
failure(error) {
62+
if (result) return false;
63+
result = { status: 1, message: error.message };
64+
return true;
65+
},
66+
});
67+
const timeout = setTimeout(() => {
68+
handle.kill();
69+
assert.fail(`WPT rejection probe did not finish: ${label}`);
70+
}, common.platformTimeout(10_000));
71+
72+
try {
73+
await handle.finished;
74+
} finally {
75+
clearTimeout(timeout);
76+
}
77+
78+
assert(result, label);
79+
assert.strictEqual(result.status, allowed ? 0 : 1, label);
80+
if (allowed) {
81+
assert.deepStrictEqual(statuses, Array(passes).fill(0), label);
82+
} else {
83+
assert.match(result.message, message, label);
84+
}
85+
}
86+
87+
(async () => {
88+
for (const backend of ['thread', 'process']) {
89+
for (const webWorker of [false, true]) {
90+
for (const [setup, allowed] of [
91+
['', false],
92+
['setup({ allow_uncaught_exception: false });', false],
93+
['setup({ allow_uncaught_exception: true });', true],
94+
['setup(() => {}, { allow_uncaught_exception: true });', true],
95+
['setup(null, { allow_uncaught_exception: true });', true],
96+
['setup({ allow_uncaught_exception: true }); setup({});', true],
97+
['setup({ allow_uncaught_exception: true }); setup({ allow_uncaught_exception: false });', false],
98+
['setup(Object.defineProperty({}, "allow_uncaught_exception", { value: true }));', false],
99+
]) {
100+
await check(backend, webWorker, {
101+
name: setup || 'default settings',
102+
code: `${setup}
103+
const t = async_test('rejection probe');
104+
Promise.reject(new Error('uncaught rejection probe'));
105+
setTimeout(() => t.done(), 0);`,
106+
allowed,
107+
});
108+
}
109+
110+
for (const allowed of [false, true]) {
111+
await check(backend, webWorker, {
112+
name: `nested setup allows rejection: ${allowed}`,
113+
code: `setup(() => setup({ allow_uncaught_exception: ${allowed} }),
114+
{ allow_uncaught_exception: ${!allowed} });
115+
const t = async_test('rejection probe');
116+
Promise.reject(new Error('uncaught rejection probe'));
117+
setTimeout(() => t.done(), 0);`,
118+
allowed,
119+
});
120+
121+
await check(backend, webWorker, {
122+
name: `late setup preserves allowance: ${allowed}`,
123+
code: `setup({ allow_uncaught_exception: ${allowed} });
124+
const t = async_test('rejection probe');
125+
test(() => {}, 'first result');
126+
setup(() => assert_unreached('late setup callback'),
127+
{ allow_uncaught_exception: ${!allowed} });
128+
Promise.reject(new Error('uncaught rejection probe'));
129+
setTimeout(() => t.done(), 0);`,
130+
allowed,
131+
passes: 2,
132+
});
133+
134+
await check(backend, webWorker, {
135+
name: `promise_setup applies deferred allowance: ${allowed}`,
136+
code: `promise_setup(async () => {}, { allow_uncaught_exception: ${allowed} });
137+
setup({ allow_uncaught_exception: ${!allowed} });
138+
promise_test(async () => {
139+
Promise.reject(new Error('uncaught rejection probe'));
140+
await new Promise(resolve => setTimeout(resolve, 0));
141+
}, 'rejection probe');`,
142+
allowed,
143+
});
144+
145+
await check(backend, webWorker, {
146+
name: `late promise_setup preserves allowance: ${allowed}`,
147+
code: `setup({ allow_uncaught_exception: ${allowed} });
148+
const t = async_test('rejection probe');
149+
test(() => {}, 'first result');
150+
promise_setup(async () => {
151+
Promise.reject(new Error('uncaught rejection probe'));
152+
setTimeout(() => t.done(), 0);
153+
}, { allow_uncaught_exception: ${!allowed} });`,
154+
allowed,
155+
passes: 2,
156+
});
157+
158+
await check(backend, webWorker, {
159+
name: `single_test still fails with allowance: ${allowed}`,
160+
code: `setup({ single_test: true, allow_uncaught_exception: ${allowed} });
161+
setup({ single_test: false });
162+
Promise.reject(new Error('uncaught rejection probe'));
163+
setTimeout(done, 0);`,
164+
allowed: false,
165+
singleTest: true,
166+
});
167+
}
168+
169+
for (const setup of [
170+
'setup(() => { throw new Error("setup failure"); }, { allow_uncaught_exception: true });',
171+
'promise_setup(async () => { throw new Error("setup failure"); }, { allow_uncaught_exception: true });',
172+
]) {
173+
await check(backend, webWorker, {
174+
name: setup,
175+
code: `${setup}\npromise_test(async () => {}, 'setup must succeed first');`,
176+
allowed: false,
177+
message: /setup failure/,
178+
});
179+
}
180+
181+
await check(backend, webWorker, {
182+
name: 'unexpected rejection fails even with a permanently pending test',
183+
code: `async_test('pending forever');
184+
Promise.reject(new Error('uncaught rejection probe'));`,
185+
allowed: false,
186+
});
187+
}
188+
}
189+
})().then(common.mustCall());

‎test/wpt/status/web-locks.json‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
11
{
2-
"held.https.any.js": {
3-
"fail": {
4-
"note": "Flaky on ppc, linux x64 and s390x #59142",
5-
"flaky": [
6-
"Error: this uncaught rejection is expected"
7-
]
8-
}
9-
},
102
"idlharness.https.any.js": {
113
"fail": {
124
"expected": [

0 commit comments

Comments
 (0)