test: support inspecting WPTs in child processes

Add WPT_INSPECT to launch one generated main-thread test with
--inspect-brk on an available port. Forward debugger stderr so an
inspector client can attach while the child is paused.

Require an exact generated test path and reject worker variants, whose
test code runs in a nested Worker. Cover backend precedence, selector
errors, inspector attachment, and clean shutdown.

Refs: https://github.com/nodejs/node/issues/51854
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65510
Reviewed-By: Aviv Keller <me@aviv.sh>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Filip Skokan authored and Node.js GitHub Bot committed 2026-08-26 12:09:42 +00:00
1 parent a15da05d61
commit 49c87ef145
4 files changed
+162 -3

No files matched your search

+32 -2
View File
@@ -754,6 +754,8 @@ function runSpecOnThread(execArgv, workerData, handlers) {
* @returns {SpecHandle}
*/
function runSpecInProcess(execArgv, workerData, handlers) {
const forwardStderr = execArgv.some(
(flag) => flag === '--inspect-brk' || flag.startsWith('--inspect-brk='));
const child = fork(workerPath, {
execArgv,
// Status files may skip subtests by regular expression, which JSON
@@ -767,6 +769,9 @@ function runSpecInProcess(execArgv, workerData, handlers) {
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk) => {
stderr += chunk;
if (forwardStderr) {
process.stderr.write(chunk);
}
});
child.on('message', (message) => {
@@ -789,7 +794,8 @@ function runSpecInProcess(execArgv, workerData, handlers) {
const name = signal ?
`Test process was killed by signal ${signal}` :
`Test process exited with code ${code}`;
if (!handlers.failure({ name, message: name, stack: stderr }) && stderr) {
if (!handlers.failure({ name, message: name, stack: stderr }) &&
stderr && !forwardStderr) {
process.stderr.write(stderr);
}
});
@@ -823,9 +829,15 @@ class WPTRunner {
concurrency = Math.min(10, concurrency);
}
this.inspectBrk = process.env.WPT_INSPECT !== undefined;
// The override exists so that every suite can be run either way without
// editing the drivers, which is how the two backends are kept compatible.
backend = process.env.WPT_BACKEND || backend;
if (this.inspectBrk) {
backend = 'process';
} else {
backend = process.env.WPT_BACKEND || backend;
}
this.runSpec = backends[backend];
if (this.runSpec === undefined) {
throw new Error(`Invalid WPT backend ${backend}, expected one of ` +
@@ -841,6 +853,9 @@ class WPTRunner {
// we enable the API globally. This has no practical
// effect on the non-web-worker tests, however.
this.flags = ['--experimental-web-worker'];
if (this.inspectBrk) {
this.flags.push('--inspect-brk=0');
}
this.globalThisInitScripts = [];
this.initScript = null;
@@ -1299,6 +1314,9 @@ class WPTRunner {
const queue = [];
this.skippedSpecCount = 0;
const arg = process.argv[2];
if (this.inspectBrk && !arg) {
throw new Error('WPT_INSPECT requires a WPT test path');
}
for (const spec of this.specs) {
if (arg) {
if (spec.isSelectedBy(arg)) {
@@ -1330,6 +1348,18 @@ class WPTRunner {
if (arg && queue.length === 0) {
throw new Error(`${arg} not found!`);
}
if (this.inspectBrk && queue.length !== 1) {
const matches = queue.map((spec) => spec.getTestPath()).join('\n');
throw new Error(
`WPT_INSPECT requires exactly one generated WPT test path; ` +
`${arg} matched ${queue.length}:\n${matches}`,
);
}
if (this.inspectBrk && queue[0].isWebWorkerTest()) {
throw new Error(
`WPT_INSPECT does not support worker tests: ${queue[0].getTestPath()}`,
);
}
return queue;
}
+71 -1
View File
@@ -13,6 +13,7 @@ const { spawnSync } = require('child_process');
const { backends, WPTRunner } = require('../common/wpt');
const queueProbe = process.env.NODE_TEST_WPT_QUEUE_PROBE === '1';
const backendProbe = process.env.NODE_TEST_WPT_BACKEND_PROBE;
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
const specPath = fixtures.path('wpt-backends-spec.js');
@@ -156,7 +157,74 @@ function checkQueuedSpecsKeepRunnerAlive() {
assert.strictEqual(status, 0, `Queued WPT probe failed:\n${stdout}${stderr}`);
}
function runDefaultBackendProbe() {
const runner = new WPTRunner('compression');
assert.strictEqual(runner.runSpec, backends[backendProbe]);
}
function checkBackendSelection() {
const env = { ...process.env };
delete env.WPT_BACKEND;
delete env.WPT_INSPECT;
for (const [name, expected, args, overrides] of [
['default', 'thread', [__filename]],
['environment override', 'process', [__filename], { WPT_BACKEND: 'process' }],
['inspect override', 'process', [__filename], {
WPT_BACKEND: 'thread',
WPT_INSPECT: '1',
}],
]) {
const result = spawnSync(process.execPath, args, {
env: {
...env,
...overrides,
NODE_TEST_WPT_BACKEND_PROBE: expected,
},
encoding: 'utf8',
timeout: common.platformTimeout(10_000),
});
const { error, status, stdout, stderr } = result;
assert.ifError(error);
assert.strictEqual(
status,
0,
`${name} WPT backend probe failed:\n${stdout}${stderr}`,
);
}
}
function checkInspectSelection() {
const driver = path.join(__dirname, '../wpt/test-compression.js');
const env = { ...process.env, WPT_INSPECT: '1' };
delete env.WPT_BACKEND;
const runFailure = common.mustCall((args, expected) => {
const result = spawnSync(process.execPath, [driver, ...args], {
env,
encoding: 'utf8',
timeout: common.platformTimeout(10_000),
});
const { error, status, stdout, stderr } = result;
assert.ifError(error);
assert.strictEqual(status, 1, `WPT inspect probe passed:\n${stdout}${stderr}`);
assert.match(stderr, expected);
}, 3);
runFailure([], /WPT_INSPECT requires a WPT test path/);
runFailure(
['compression-bad-chunks.any.js'],
/WPT_INSPECT requires exactly one generated WPT test path; .* matched 2:\r?\ncompression\/compression-bad-chunks\.any\.html\r?\ncompression\/compression-bad-chunks\.any\.worker\.html/,
);
runFailure(
['compression/compression-bad-chunks.any.worker.html'],
/WPT_INSPECT does not support worker tests: compression\/compression-bad-chunks\.any\.worker\.html/,
);
}
async function main() {
checkBackendSelection();
checkInspectSelection();
checkQueuedSpecsKeepRunnerAlive();
const completed = await compare(false);
@@ -197,7 +265,9 @@ async function main() {
assert.deepStrictEqual(workerResults, windowResults);
}
if (queueProbe) {
if (backendProbe) {
runDefaultBackendProbe();
} else if (queueProbe) {
runQueueProbe();
} else {
main().then(common.mustCall());
+44
View File
@@ -0,0 +1,44 @@
'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
const assert = require('assert');
const path = require('path');
const { NodeInstance } = require('../common/inspector-helper');
const driver = path.join(__dirname, '../wpt/test-compression.js');
async function main() {
const parent = new NodeInstance([], `
delete process.env.WPT_BACKEND;
process.env.WPT_INSPECT = '1';
process.argv[2] = 'compression/compression-bad-chunks.any.html';
require(${JSON.stringify(driver)});
`, '', {
log() {},
error() {},
});
const stderr = [];
parent.on('stderr', (line) => stderr.push(line));
const session = await parent.connectInspectorSession();
await session.send([
{ method: 'Runtime.enable' },
{ method: 'Debugger.enable' },
{ method: 'Runtime.runIfWaitingForDebugger' },
]);
await session.waitForNotification('Debugger.paused');
await session.send({ method: 'Debugger.resume' });
await session.disconnect();
const { exitCode, signal } = await parent.expectShutdown();
assert.strictEqual(signal, null);
assert.strictEqual(exitCode, 0);
assert.strictEqual(
stderr.filter((line) => line.startsWith('Debugger listening on ')).length,
1,
);
}
main().then(common.mustCall());
+15
View File
@@ -42,6 +42,20 @@ tests in child processes instead:
WPT_BACKEND=process tools/test.py wpt
```
### Debugging a test
Set `WPT_INSPECT=1` to run one generated main-thread test in a child process
with `--inspect-brk` on an available port:
```bash
WPT_INSPECT=1 out/Release/node test/wpt/test-compression.js \
'compression/compression-bad-chunks.any.html'
```
Connect an inspector client to the URL printed on stderr. A source file that
generates multiple tests is rejected with the exact paths to choose from.
Worker tests are not supported by inspect mode.
<a id="add-tests"></a>
## How to add tests for a new module
@@ -96,6 +110,7 @@ selected backend.
that require sequential execution (e.g. web-locks, webstorage).
* `backend` {string} Test execution backend. Must be either `'thread'` or
`'process'`. Defaults to `'thread'`. `WPT_BACKEND` overrides this option.
`WPT_INSPECT` always uses `'process'`.
#### `runner.setFlags(flags)`