mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
test: enable multi-global WPTs
Signed-off-by: Filip Skokan <panva.ip@gmail.com> Co-authored-by: Aviv Keller <me@aviv.sh> Signed-off-by: Aviv Keller <me@aviv.sh> PR-URL: https://github.com/nodejs/node/pull/64894 Fixes: https://github.com/nodejs/node/issues/43583 Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matthew Aitken <maitken033380023@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
1 parent
b9afdd0b87
commit
78cca3671f
11 files changed
+364
-31
No files matched your search
+59
-20
@@ -8,11 +8,11 @@ const path = require('path');
|
||||
const events = require('events');
|
||||
const os = require('os');
|
||||
const { inspect } = require('util');
|
||||
const { pathToFileURL } = require('url');
|
||||
const { Worker } = require('worker_threads');
|
||||
const { fork } = require('child_process');
|
||||
|
||||
const workerPath = path.join(__dirname, 'wpt/worker.js');
|
||||
const kRunWorkerGlobals = false;
|
||||
const wptNonTestDirs = new Set(['resources', 'support', 'tools']);
|
||||
|
||||
function getBrowserProperties() {
|
||||
@@ -191,6 +191,24 @@ class ResourceLoader {
|
||||
fixtures.path('wpt', base, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a URL that a test would have fetched from the WPT server (an
|
||||
* absolute path, or a path relative to the test file) to a file: URL
|
||||
* into the fixtures directory. URLs that already have a scheme (data:,
|
||||
* blob:, http:, ...) are returned unchanged.
|
||||
* @param {string} from the path of the file loading this resource,
|
||||
* relative to the WPT folder.
|
||||
* @param {string|URL} url the url of the resource being loaded.
|
||||
* @returns {string}
|
||||
*/
|
||||
mapServerURL(from, url) {
|
||||
url = `${url}`;
|
||||
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:|^\/\//.test(url)) {
|
||||
return url;
|
||||
}
|
||||
return pathToFileURL(this.toRealFilePath(from, url)).href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a resource in test/fixtures/wpt specified with a URL
|
||||
* @param {string} from the path of the file loading this resource,
|
||||
@@ -818,7 +836,11 @@ class WPTRunner {
|
||||
this.resource = new ResourceLoader(path);
|
||||
this.concurrency = concurrency;
|
||||
|
||||
this.flags = [];
|
||||
// Since we need to prepare the Web Worker APIs
|
||||
// in the harness that runs on all WPT workers,
|
||||
// we enable the API globally. This has no practical
|
||||
// effect on the non-web-worker tests, however.
|
||||
this.flags = ['--experimental-web-worker'];
|
||||
this.globalThisInitScripts = [];
|
||||
this.initScript = null;
|
||||
|
||||
@@ -845,7 +867,7 @@ class WPTRunner {
|
||||
* @param {string[]} flags
|
||||
*/
|
||||
setFlags(flags) {
|
||||
this.flags = flags;
|
||||
this.flags = this.flags.concat(flags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -933,8 +955,17 @@ class WPTRunner {
|
||||
const absolutePath = spec.getAbsolutePath();
|
||||
const relativePath = spec.getRelativePath();
|
||||
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
|
||||
// Scripts specified with the `// META: script=` header
|
||||
const scriptsToRun = meta.script?.map((script) => {
|
||||
// *.worker.js tests are dedicated worker tests by definition. Each
|
||||
// dedicated worker variant generated from a multi-global (*.any.js)
|
||||
// test also runs inside an actual Web Worker. Refs:
|
||||
// https://web-platform-tests.org/writing-tests/testharness.html#multi-global-tests
|
||||
const isAnyTest = spec.isAnyTest();
|
||||
const isWebWorkerTest = spec.isWebWorkerTest();
|
||||
|
||||
// Scripts specified with the `// META: script=` header. For tests
|
||||
// that run inside a Web Worker they are imported by the worker
|
||||
// instead.
|
||||
const scriptsToRun = isWebWorkerTest ? [] : meta.script?.map((script) => {
|
||||
const obj = {
|
||||
filename: this.resource.toRealFilePath(relativePath, script),
|
||||
code: this.resource.read(relativePath, script),
|
||||
@@ -942,13 +973,15 @@ class WPTRunner {
|
||||
this.scriptsModifier?.(obj);
|
||||
return obj;
|
||||
}) ?? [];
|
||||
// The actual test
|
||||
const obj = {
|
||||
code: content,
|
||||
filename: absolutePath,
|
||||
};
|
||||
this.scriptsModifier?.(obj);
|
||||
scriptsToRun.push(obj);
|
||||
if (!isWebWorkerTest) {
|
||||
// The actual test
|
||||
const obj = {
|
||||
code: content,
|
||||
filename: absolutePath,
|
||||
};
|
||||
this.scriptsModifier?.(obj);
|
||||
scriptsToRun.push(obj);
|
||||
}
|
||||
|
||||
jobs.push(run(async () => {
|
||||
this.inProgress.add(spec);
|
||||
@@ -964,6 +997,18 @@ class WPTRunner {
|
||||
filename: harnessPath,
|
||||
},
|
||||
scriptsToRun,
|
||||
// Set when the test runs inside an actual Web Worker.
|
||||
webWorker: isWebWorkerTest ? {
|
||||
path: absolutePath,
|
||||
isAnyTest,
|
||||
initScript: this.initScript,
|
||||
title: meta.title,
|
||||
variant: spec.variant,
|
||||
scripts: meta.script?.map(
|
||||
(script) => this.resource.toRealFilePath(relativePath, script),
|
||||
) ?? [],
|
||||
skippedTests: spec.skippedTests,
|
||||
} : undefined,
|
||||
needsGc: !!meta.script?.find((script) => script === '/common/gc.js'),
|
||||
skippedTests: spec.skippedTests,
|
||||
}, {
|
||||
@@ -1255,10 +1300,6 @@ class WPTRunner {
|
||||
this.skippedSpecCount = 0;
|
||||
const arg = process.argv[2];
|
||||
for (const spec of this.specs) {
|
||||
if (!kRunWorkerGlobals && spec.isWebWorkerTest()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg) {
|
||||
if (spec.isSelectedBy(arg)) {
|
||||
queue.push(spec);
|
||||
@@ -1281,13 +1322,11 @@ class WPTRunner {
|
||||
}
|
||||
|
||||
// If the tests are run as `node test/wpt/test-something.js subset.any.js`,
|
||||
// only `subset.any.js` (all enabled variants and globals) will be run by
|
||||
// the runner.
|
||||
// only `subset.any.js` (all variants and globals) will be run by the runner.
|
||||
// If the tests are run as `node test/wpt/test-something.js 'subset.any.js?1-10'`,
|
||||
// only the `?1-10` variant of `subset.any.js` will be run by the runner.
|
||||
// A test path as printed with the results, e.g.
|
||||
// `'dir/subset.any.worker.html?1-10'`, runs exactly that one when its
|
||||
// global is enabled.
|
||||
// `'dir/subset.any.worker.html?1-10'`, runs exactly that one.
|
||||
if (arg && queue.length === 0) {
|
||||
throw new Error(`${arg} not found!`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
'use strict';
|
||||
|
||||
// Runs a WPT test file inside a Web Worker
|
||||
// Refs: https://web-platform-tests.org/writing-tests/testharness.html
|
||||
|
||||
const { pathToFileURL } = require('url');
|
||||
const {
|
||||
runInThisContext,
|
||||
constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER },
|
||||
} = require('vm');
|
||||
|
||||
globalThis.onmessage = ({ data }) => {
|
||||
// Let the test install its own handler.
|
||||
globalThis.onmessage = null;
|
||||
|
||||
const { ResourceLoader } = require(data.wptRunner);
|
||||
const resource = new ResourceLoader(data.wptPath);
|
||||
|
||||
globalThis.fetch = function fetch(file) {
|
||||
return resource.readAsFetch(data.testRelativePath, file);
|
||||
};
|
||||
|
||||
// Pretend the worker was served from the URL the WPT server would have
|
||||
// used
|
||||
const fakePath = (data.isAnyTest ?
|
||||
data.testRelativePath.replace(/\.any\.js$/, '.any.worker.js') :
|
||||
data.testRelativePath).replace(/\\/g, '/');
|
||||
const fakeURL = new URL(`/${fakePath}${data.variant}`, 'http://wpt');
|
||||
// eslint-disable-next-line no-undef
|
||||
const fakeLocation = { __proto__: WorkerLocation.prototype };
|
||||
for (const key of ['href', 'origin', 'protocol', 'host', 'hostname',
|
||||
'port', 'pathname', 'search', 'hash']) {
|
||||
Object.defineProperty(fakeLocation, key, {
|
||||
value: fakeURL[key],
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
Object.defineProperty(fakeLocation, 'toString', {
|
||||
value: function toString() { return fakeURL.href; },
|
||||
enumerable: true,
|
||||
});
|
||||
Object.defineProperty(globalThis, 'location', {
|
||||
value: fakeLocation,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const testharnessPath =
|
||||
pathToFileURL(resource.toRealFilePath(data.testRelativePath,
|
||||
'/resources/testharness.js')).href;
|
||||
|
||||
// If there are skip patterns, wrap the test functions to prevent
|
||||
// execution of matching tests. This must happen after testharness.js is
|
||||
// loaded but before the test scripts run.
|
||||
function applySkips() {
|
||||
if (!data.skippedTests?.length) {
|
||||
return;
|
||||
}
|
||||
function isSkipped(name) {
|
||||
for (const matcher of data.skippedTests) {
|
||||
if (typeof matcher === 'string') {
|
||||
if (name === matcher) return true;
|
||||
} else if (matcher.test(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
for (const fn of ['test', 'async_test', 'promise_test']) {
|
||||
const original = globalThis[fn];
|
||||
globalThis[fn] = function(func, name, ...rest) {
|
||||
if (typeof name === 'string' && isSkipped(name)) {
|
||||
// eslint-disable-next-line no-undef
|
||||
postMessage({ type: 'skip', name });
|
||||
return;
|
||||
}
|
||||
return original.call(this, func, name, ...rest);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Tests fetch scripts and nested worker scripts from the WPT server; map
|
||||
// those URLs into the fixtures directory.
|
||||
const realImportScripts = globalThis.importScripts;
|
||||
globalThis.importScripts = function importScripts(...urls) {
|
||||
const mapped = urls.map(
|
||||
(url) => resource.mapServerURL(data.testRelativePath, url));
|
||||
const result = realImportScripts.apply(this, mapped);
|
||||
if (mapped.includes(testharnessPath)) {
|
||||
applySkips();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const RealWorker = globalThis.Worker;
|
||||
globalThis.Worker = class Worker extends RealWorker {
|
||||
constructor(url, options) {
|
||||
super(resource.mapServerURL(data.testRelativePath, url), options);
|
||||
}
|
||||
};
|
||||
|
||||
if (data.isAnyTest) {
|
||||
// Emulate the generated .any.worker.js wrapper script.
|
||||
// Refs: https://github.com/web-platform-tests/wpt/blob/master/tools/serve/serve.py
|
||||
globalThis.GLOBAL = {
|
||||
isWindow() { return false; },
|
||||
isWorker() { return true; },
|
||||
isShadowRealm() { return false; },
|
||||
};
|
||||
}
|
||||
|
||||
if (data.initScript) {
|
||||
runInThisContext(data.initScript, {
|
||||
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.title) {
|
||||
globalThis.META_TITLE = data.title;
|
||||
}
|
||||
|
||||
if (data.isAnyTest) {
|
||||
globalThis.importScripts('/resources/testharness.js');
|
||||
for (const script of data.scripts) {
|
||||
globalThis.importScripts(pathToFileURL(script).href);
|
||||
}
|
||||
globalThis.importScripts(pathToFileURL(data.path).href);
|
||||
// eslint-disable-next-line no-undef
|
||||
done();
|
||||
} else {
|
||||
// *.worker.js tests import testharness.js and call done() themselves.
|
||||
globalThis.importScripts(pathToFileURL(data.path).href);
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const { pathToFileURL } = require('url');
|
||||
const {
|
||||
runInNewContext,
|
||||
runInThisContext,
|
||||
@@ -38,6 +40,15 @@ function run(workerData) {
|
||||
const { ResourceLoader } = require(workerData.wptRunner);
|
||||
const resource = new ResourceLoader(workerData.wptPath);
|
||||
|
||||
// Tests create workers with URLs the WPT server would have served them
|
||||
// from; map them into the fixtures directory.
|
||||
const RealWorker = globalThis.Worker;
|
||||
globalThis.Worker = class Worker extends RealWorker {
|
||||
constructor(url, options) {
|
||||
super(resource.mapServerURL(workerData.testRelativePath, url), options);
|
||||
}
|
||||
};
|
||||
|
||||
if (workerData.needsGc) {
|
||||
// See https://github.com/nodejs/node/issues/16595#issuecomment-340288680
|
||||
setFlagsFromString('--expose-gc');
|
||||
@@ -131,4 +142,44 @@ function run(workerData) {
|
||||
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
||||
});
|
||||
}
|
||||
|
||||
if (workerData.webWorker) {
|
||||
const worker = new RealWorker(
|
||||
pathToFileURL(path.join(__dirname, 'webworker.js')));
|
||||
worker.postMessage({
|
||||
wptRunner: workerData.wptRunner,
|
||||
wptPath: workerData.wptPath,
|
||||
testRelativePath: workerData.testRelativePath,
|
||||
...workerData.webWorker,
|
||||
});
|
||||
|
||||
let completed = false;
|
||||
worker.addEventListener('message', (event) => {
|
||||
if (event.data?.type === 'complete') {
|
||||
completed = true;
|
||||
}
|
||||
// Skipped subtests never register with the testharness inside the
|
||||
// worker; the runner is notified about them directly.
|
||||
if (event.data?.type === 'skip') {
|
||||
send({ type: 'skip', name: event.data.name });
|
||||
}
|
||||
});
|
||||
worker.addEventListener('error', (event) => {
|
||||
if (completed) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
send({
|
||||
type: 'completion',
|
||||
status: {
|
||||
status: 1,
|
||||
message: event.message,
|
||||
stack: event.error?.stack,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
fetch_tests_from_worker(worker);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
// Throw only once a result has been reported, so the harness has to surface
|
||||
// the error rather than treat the test file as already done.
|
||||
add_result_callback(() => {
|
||||
setTimeout(() => {
|
||||
throw new Error('probe error after first result');
|
||||
}, 0);
|
||||
});
|
||||
|
||||
test(() => {}, 'reported before error');
|
||||
async_test(() => {}, 'waiting for error');
|
||||
@@ -16,7 +16,7 @@ const queueProbe = process.env.NODE_TEST_WPT_QUEUE_PROBE === '1';
|
||||
|
||||
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
|
||||
const specPath = fixtures.path('wpt-backends-spec.js');
|
||||
const execArgv = [];
|
||||
const execArgv = ['--experimental-web-worker'];
|
||||
|
||||
function payload(throws) {
|
||||
return {
|
||||
@@ -175,6 +175,18 @@ async function main() {
|
||||
runDriver(driver, spec, 'process'),
|
||||
);
|
||||
}
|
||||
|
||||
const windowResults = runDriver(
|
||||
'test-events.js',
|
||||
'dom/events/Event-constructors.any.html',
|
||||
'thread',
|
||||
);
|
||||
const workerResults = runDriver(
|
||||
'test-events.js',
|
||||
'dom/events/Event-constructors.any.worker.html',
|
||||
'thread',
|
||||
).map((line) => line.replace('.any.worker.html', '.any.html'));
|
||||
assert.deepStrictEqual(workerResults, windowResults);
|
||||
}
|
||||
|
||||
if (queueProbe) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { fork } = require('child_process');
|
||||
const { Worker } = require('worker_threads');
|
||||
|
||||
const workerPath = path.join(__dirname, '../common/wpt/worker.js');
|
||||
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
|
||||
const execArgv = ['--experimental-web-worker'];
|
||||
const workerData = {
|
||||
testRelativePath: 'workers/error-after-result.any.js',
|
||||
wptRunner: path.join(__dirname, '../common/wpt.js'),
|
||||
wptPath: 'workers',
|
||||
initScript: null,
|
||||
harness: {
|
||||
code: fs.readFileSync(harnessPath, 'utf8'),
|
||||
filename: harnessPath,
|
||||
},
|
||||
scriptsToRun: [],
|
||||
webWorker: {
|
||||
path: fixtures.path('web-worker', 'wpt-error-after-result.js'),
|
||||
isAnyTest: true,
|
||||
initScript: null,
|
||||
variant: '',
|
||||
scripts: [],
|
||||
skippedTests: [],
|
||||
},
|
||||
needsGc: false,
|
||||
skippedTests: [],
|
||||
};
|
||||
|
||||
// Both backends have to report an error thrown by a Web Worker after a result
|
||||
// was already reported, and report it the same way.
|
||||
check('thread', () => {
|
||||
const worker = new Worker(workerPath, { execArgv, workerData });
|
||||
return { runner: worker, stop: () => worker.terminate() };
|
||||
});
|
||||
check('process', () => {
|
||||
const child = fork(workerPath, { execArgv, serialization: 'advanced' });
|
||||
child.send(workerData);
|
||||
return { runner: child, stop: () => child.kill() };
|
||||
});
|
||||
|
||||
function check(backend, start) {
|
||||
const { runner, stop } = start();
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
stop();
|
||||
assert.fail(`WPT worker error was not reported on the ${backend} backend`);
|
||||
}, common.platformTimeout(10_000));
|
||||
|
||||
const onCompletion = common.mustCall((status) => {
|
||||
clearTimeout(timeout);
|
||||
assert.strictEqual(status.status, 1);
|
||||
assert.strictEqual(status.message, 'probe error after first result');
|
||||
stop();
|
||||
});
|
||||
|
||||
runner.on('error', common.mustNotCall());
|
||||
// The error reaches the runner straight from the Web Worker while results
|
||||
// travel through the harness, so it can arrive before them.
|
||||
runner.on('message', common.mustCallAtLeast((message) => {
|
||||
switch (message.type) {
|
||||
case 'result':
|
||||
assert.strictEqual(message.result.name, 'reported before error');
|
||||
assert.strictEqual(message.result.status, 0);
|
||||
break;
|
||||
case 'completion':
|
||||
onCompletion(message.status);
|
||||
break;
|
||||
default:
|
||||
assert.fail(`Unexpected message type: ${message.type}`);
|
||||
}
|
||||
}, 1));
|
||||
}
|
||||
@@ -33,7 +33,4 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
},
|
||||
'Blob-in-worker.worker.js': {
|
||||
skip: 'Depends on Web Workers API',
|
||||
},
|
||||
};
|
||||
@@ -21,9 +21,6 @@
|
||||
"event-global-set-before-handleEvent-lookup.window.js": {
|
||||
"skip": "window is not defined"
|
||||
},
|
||||
"event-global.worker.js": {
|
||||
"skip": "importScripts is not defined"
|
||||
},
|
||||
"legacy-pre-activation-behavior.window.js": {
|
||||
"skip": "document is not defined"
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ module.exports = {
|
||||
},
|
||||
} : {}),
|
||||
|
||||
'idlharness.any.js': {
|
||||
'idlharness.any.html': {
|
||||
fail: {
|
||||
expected: [
|
||||
'Window interface: attribute performance',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
'historical.any.js': {
|
||||
'historical.any.html': {
|
||||
'fail': {
|
||||
'expected': [
|
||||
'searchParams on location object',
|
||||
|
||||
@@ -27,11 +27,24 @@
|
||||
"Global object's setter throws when called on incompatible object",
|
||||
"Global object's operation throws when called on incompatible object",
|
||||
"Global object's getter works when called on null / undefined",
|
||||
"Global object's setter works when called on null / undefined",
|
||||
"Global object's setter works when called on null / undefined"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ecmascript-binding/global-object-implicit-this-value.any.html": {
|
||||
"fail": {
|
||||
"expected": [
|
||||
"ReferenceError: addEventListener is not defined"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ecmascript-binding/global-object-implicit-this-value.any.worker.html": {
|
||||
"fail": {
|
||||
"expected": [
|
||||
"Global object's operation works when called on null / undefined"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ecmascript-binding/legacy-factor-function-subclass.window.js": {
|
||||
"fail": {
|
||||
"expected": [
|
||||
@@ -46,7 +59,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"ecmascript-binding/no-regexp-special-casing.any.js": {
|
||||
"ecmascript-binding/no-regexp-special-casing.any.html": {
|
||||
"fail": {
|
||||
"expected": [
|
||||
"Can be used as an object implementing a callback interface"
|
||||
|
||||
Reference in new issue
Block a user