Skip to content

Commit 78cca36

Browse files
panvaavivkeller
andcommitted
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: #64894 Fixes: #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>
1 parent b9afdd0 commit 78cca36

11 files changed

Lines changed: 364 additions & 31 deletions

File tree

test/common/wpt.js

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ const path = require('path');
88
const events = require('events');
99
const os = require('os');
1010
const { inspect } = require('util');
11+
const { pathToFileURL } = require('url');
1112
const { Worker } = require('worker_threads');
1213
const { fork } = require('child_process');
1314

1415
const workerPath = path.join(__dirname, 'wpt/worker.js');
15-
const kRunWorkerGlobals = false;
1616
const wptNonTestDirs = new Set(['resources', 'support', 'tools']);
1717

1818
function getBrowserProperties() {
@@ -191,6 +191,24 @@ class ResourceLoader {
191191
fixtures.path('wpt', base, url);
192192
}
193193

194+
/**
195+
* Map a URL that a test would have fetched from the WPT server (an
196+
* absolute path, or a path relative to the test file) to a file: URL
197+
* into the fixtures directory. URLs that already have a scheme (data:,
198+
* blob:, http:, ...) are returned unchanged.
199+
* @param {string} from the path of the file loading this resource,
200+
* relative to the WPT folder.
201+
* @param {string|URL} url the url of the resource being loaded.
202+
* @returns {string}
203+
*/
204+
mapServerURL(from, url) {
205+
url = `${url}`;
206+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:|^\/\//.test(url)) {
207+
return url;
208+
}
209+
return pathToFileURL(this.toRealFilePath(from, url)).href;
210+
}
211+
194212
/**
195213
* Load a resource in test/fixtures/wpt specified with a URL
196214
* @param {string} from the path of the file loading this resource,
@@ -818,7 +836,11 @@ class WPTRunner {
818836
this.resource = new ResourceLoader(path);
819837
this.concurrency = concurrency;
820838

821-
this.flags = [];
839+
// Since we need to prepare the Web Worker APIs
840+
// in the harness that runs on all WPT workers,
841+
// we enable the API globally. This has no practical
842+
// effect on the non-web-worker tests, however.
843+
this.flags = ['--experimental-web-worker'];
822844
this.globalThisInitScripts = [];
823845
this.initScript = null;
824846

@@ -845,7 +867,7 @@ class WPTRunner {
845867
* @param {string[]} flags
846868
*/
847869
setFlags(flags) {
848-
this.flags = flags;
870+
this.flags = this.flags.concat(flags);
849871
}
850872

851873
/**
@@ -933,22 +955,33 @@ class WPTRunner {
933955
const absolutePath = spec.getAbsolutePath();
934956
const relativePath = spec.getRelativePath();
935957
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
936-
// Scripts specified with the `// META: script=` header
937-
const scriptsToRun = meta.script?.map((script) => {
958+
// *.worker.js tests are dedicated worker tests by definition. Each
959+
// dedicated worker variant generated from a multi-global (*.any.js)
960+
// test also runs inside an actual Web Worker. Refs:
961+
// https://web-platform-tests.org/writing-tests/testharness.html#multi-global-tests
962+
const isAnyTest = spec.isAnyTest();
963+
const isWebWorkerTest = spec.isWebWorkerTest();
964+
965+
// Scripts specified with the `// META: script=` header. For tests
966+
// that run inside a Web Worker they are imported by the worker
967+
// instead.
968+
const scriptsToRun = isWebWorkerTest ? [] : meta.script?.map((script) => {
938969
const obj = {
939970
filename: this.resource.toRealFilePath(relativePath, script),
940971
code: this.resource.read(relativePath, script),
941972
};
942973
this.scriptsModifier?.(obj);
943974
return obj;
944975
}) ?? [];
945-
// The actual test
946-
const obj = {
947-
code: content,
948-
filename: absolutePath,
949-
};
950-
this.scriptsModifier?.(obj);
951-
scriptsToRun.push(obj);
976+
if (!isWebWorkerTest) {
977+
// The actual test
978+
const obj = {
979+
code: content,
980+
filename: absolutePath,
981+
};
982+
this.scriptsModifier?.(obj);
983+
scriptsToRun.push(obj);
984+
}
952985

953986
jobs.push(run(async () => {
954987
this.inProgress.add(spec);
@@ -964,6 +997,18 @@ class WPTRunner {
964997
filename: harnessPath,
965998
},
966999
scriptsToRun,
1000+
// Set when the test runs inside an actual Web Worker.
1001+
webWorker: isWebWorkerTest ? {
1002+
path: absolutePath,
1003+
isAnyTest,
1004+
initScript: this.initScript,
1005+
title: meta.title,
1006+
variant: spec.variant,
1007+
scripts: meta.script?.map(
1008+
(script) => this.resource.toRealFilePath(relativePath, script),
1009+
) ?? [],
1010+
skippedTests: spec.skippedTests,
1011+
} : undefined,
9671012
needsGc: !!meta.script?.find((script) => script === '/common/gc.js'),
9681013
skippedTests: spec.skippedTests,
9691014
}, {
@@ -1255,10 +1300,6 @@ class WPTRunner {
12551300
this.skippedSpecCount = 0;
12561301
const arg = process.argv[2];
12571302
for (const spec of this.specs) {
1258-
if (!kRunWorkerGlobals && spec.isWebWorkerTest()) {
1259-
continue;
1260-
}
1261-
12621303
if (arg) {
12631304
if (spec.isSelectedBy(arg)) {
12641305
queue.push(spec);
@@ -1281,13 +1322,11 @@ class WPTRunner {
12811322
}
12821323

12831324
// If the tests are run as `node test/wpt/test-something.js subset.any.js`,
1284-
// only `subset.any.js` (all enabled variants and globals) will be run by
1285-
// the runner.
1325+
// only `subset.any.js` (all variants and globals) will be run by the runner.
12861326
// If the tests are run as `node test/wpt/test-something.js 'subset.any.js?1-10'`,
12871327
// only the `?1-10` variant of `subset.any.js` will be run by the runner.
12881328
// A test path as printed with the results, e.g.
1289-
// `'dir/subset.any.worker.html?1-10'`, runs exactly that one when its
1290-
// global is enabled.
1329+
// `'dir/subset.any.worker.html?1-10'`, runs exactly that one.
12911330
if (arg && queue.length === 0) {
12921331
throw new Error(`${arg} not found!`);
12931332
}

test/common/wpt/webworker.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use strict';
2+
3+
// Runs a WPT test file inside a Web Worker
4+
// Refs: https://web-platform-tests.org/writing-tests/testharness.html
5+
6+
const { pathToFileURL } = require('url');
7+
const {
8+
runInThisContext,
9+
constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER },
10+
} = require('vm');
11+
12+
globalThis.onmessage = ({ data }) => {
13+
// Let the test install its own handler.
14+
globalThis.onmessage = null;
15+
16+
const { ResourceLoader } = require(data.wptRunner);
17+
const resource = new ResourceLoader(data.wptPath);
18+
19+
globalThis.fetch = function fetch(file) {
20+
return resource.readAsFetch(data.testRelativePath, file);
21+
};
22+
23+
// Pretend the worker was served from the URL the WPT server would have
24+
// used
25+
const fakePath = (data.isAnyTest ?
26+
data.testRelativePath.replace(/\.any\.js$/, '.any.worker.js') :
27+
data.testRelativePath).replace(/\\/g, '/');
28+
const fakeURL = new URL(`/${fakePath}${data.variant}`, 'http://wpt');
29+
// eslint-disable-next-line no-undef
30+
const fakeLocation = { __proto__: WorkerLocation.prototype };
31+
for (const key of ['href', 'origin', 'protocol', 'host', 'hostname',
32+
'port', 'pathname', 'search', 'hash']) {
33+
Object.defineProperty(fakeLocation, key, {
34+
value: fakeURL[key],
35+
enumerable: true,
36+
});
37+
}
38+
Object.defineProperty(fakeLocation, 'toString', {
39+
value: function toString() { return fakeURL.href; },
40+
enumerable: true,
41+
});
42+
Object.defineProperty(globalThis, 'location', {
43+
value: fakeLocation,
44+
enumerable: true,
45+
configurable: true,
46+
});
47+
48+
const testharnessPath =
49+
pathToFileURL(resource.toRealFilePath(data.testRelativePath,
50+
'/resources/testharness.js')).href;
51+
52+
// If there are skip patterns, wrap the test functions to prevent
53+
// execution of matching tests. This must happen after testharness.js is
54+
// loaded but before the test scripts run.
55+
function applySkips() {
56+
if (!data.skippedTests?.length) {
57+
return;
58+
}
59+
function isSkipped(name) {
60+
for (const matcher of data.skippedTests) {
61+
if (typeof matcher === 'string') {
62+
if (name === matcher) return true;
63+
} else if (matcher.test(name)) {
64+
return true;
65+
}
66+
}
67+
return false;
68+
}
69+
for (const fn of ['test', 'async_test', 'promise_test']) {
70+
const original = globalThis[fn];
71+
globalThis[fn] = function(func, name, ...rest) {
72+
if (typeof name === 'string' && isSkipped(name)) {
73+
// eslint-disable-next-line no-undef
74+
postMessage({ type: 'skip', name });
75+
return;
76+
}
77+
return original.call(this, func, name, ...rest);
78+
};
79+
}
80+
}
81+
82+
// Tests fetch scripts and nested worker scripts from the WPT server; map
83+
// those URLs into the fixtures directory.
84+
const realImportScripts = globalThis.importScripts;
85+
globalThis.importScripts = function importScripts(...urls) {
86+
const mapped = urls.map(
87+
(url) => resource.mapServerURL(data.testRelativePath, url));
88+
const result = realImportScripts.apply(this, mapped);
89+
if (mapped.includes(testharnessPath)) {
90+
applySkips();
91+
}
92+
return result;
93+
};
94+
const RealWorker = globalThis.Worker;
95+
globalThis.Worker = class Worker extends RealWorker {
96+
constructor(url, options) {
97+
super(resource.mapServerURL(data.testRelativePath, url), options);
98+
}
99+
};
100+
101+
if (data.isAnyTest) {
102+
// Emulate the generated .any.worker.js wrapper script.
103+
// Refs: https://github.com/web-platform-tests/wpt/blob/master/tools/serve/serve.py
104+
globalThis.GLOBAL = {
105+
isWindow() { return false; },
106+
isWorker() { return true; },
107+
isShadowRealm() { return false; },
108+
};
109+
}
110+
111+
if (data.initScript) {
112+
runInThisContext(data.initScript, {
113+
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
114+
});
115+
}
116+
117+
if (data.title) {
118+
globalThis.META_TITLE = data.title;
119+
}
120+
121+
if (data.isAnyTest) {
122+
globalThis.importScripts('/resources/testharness.js');
123+
for (const script of data.scripts) {
124+
globalThis.importScripts(pathToFileURL(script).href);
125+
}
126+
globalThis.importScripts(pathToFileURL(data.path).href);
127+
// eslint-disable-next-line no-undef
128+
done();
129+
} else {
130+
// *.worker.js tests import testharness.js and call done() themselves.
131+
globalThis.importScripts(pathToFileURL(data.path).href);
132+
}
133+
};

test/common/wpt/worker.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
const path = require('path');
4+
const { pathToFileURL } = require('url');
35
const {
46
runInNewContext,
57
runInThisContext,
@@ -38,6 +40,15 @@ function run(workerData) {
3840
const { ResourceLoader } = require(workerData.wptRunner);
3941
const resource = new ResourceLoader(workerData.wptPath);
4042

43+
// Tests create workers with URLs the WPT server would have served them
44+
// from; map them into the fixtures directory.
45+
const RealWorker = globalThis.Worker;
46+
globalThis.Worker = class Worker extends RealWorker {
47+
constructor(url, options) {
48+
super(resource.mapServerURL(workerData.testRelativePath, url), options);
49+
}
50+
};
51+
4152
if (workerData.needsGc) {
4253
// See https://github.com/nodejs/node/issues/16595#issuecomment-340288680
4354
setFlagsFromString('--expose-gc');
@@ -131,4 +142,44 @@ function run(workerData) {
131142
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
132143
});
133144
}
145+
146+
if (workerData.webWorker) {
147+
const worker = new RealWorker(
148+
pathToFileURL(path.join(__dirname, 'webworker.js')));
149+
worker.postMessage({
150+
wptRunner: workerData.wptRunner,
151+
wptPath: workerData.wptPath,
152+
testRelativePath: workerData.testRelativePath,
153+
...workerData.webWorker,
154+
});
155+
156+
let completed = false;
157+
worker.addEventListener('message', (event) => {
158+
if (event.data?.type === 'complete') {
159+
completed = true;
160+
}
161+
// Skipped subtests never register with the testharness inside the
162+
// worker; the runner is notified about them directly.
163+
if (event.data?.type === 'skip') {
164+
send({ type: 'skip', name: event.data.name });
165+
}
166+
});
167+
worker.addEventListener('error', (event) => {
168+
if (completed) {
169+
return;
170+
}
171+
clearTimeout(timeout);
172+
send({
173+
type: 'completion',
174+
status: {
175+
status: 1,
176+
message: event.message,
177+
stack: event.error?.stack,
178+
},
179+
});
180+
});
181+
182+
// eslint-disable-next-line no-undef
183+
fetch_tests_from_worker(worker);
184+
}
134185
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict';
2+
3+
// Throw only once a result has been reported, so the harness has to surface
4+
// the error rather than treat the test file as already done.
5+
add_result_callback(() => {
6+
setTimeout(() => {
7+
throw new Error('probe error after first result');
8+
}, 0);
9+
});
10+
11+
test(() => {}, 'reported before error');
12+
async_test(() => {}, 'waiting for error');

test/parallel/test-common-wpt-backends.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const queueProbe = process.env.NODE_TEST_WPT_QUEUE_PROBE === '1';
1616

1717
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
1818
const specPath = fixtures.path('wpt-backends-spec.js');
19-
const execArgv = [];
19+
const execArgv = ['--experimental-web-worker'];
2020

2121
function payload(throws) {
2222
return {
@@ -175,6 +175,18 @@ async function main() {
175175
runDriver(driver, spec, 'process'),
176176
);
177177
}
178+
179+
const windowResults = runDriver(
180+
'test-events.js',
181+
'dom/events/Event-constructors.any.html',
182+
'thread',
183+
);
184+
const workerResults = runDriver(
185+
'test-events.js',
186+
'dom/events/Event-constructors.any.worker.html',
187+
'thread',
188+
).map((line) => line.replace('.any.worker.html', '.any.html'));
189+
assert.deepStrictEqual(workerResults, windowResults);
178190
}
179191

180192
if (queueProbe) {

0 commit comments

Comments
 (0)