Skip to content

Commit a165ba7

Browse files
panvaavivkeller
authored andcommitted
test: accomodate multi-global tests in WPT{Runner,TestSpec,Report}
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent e41d8f3 commit a165ba7

6 files changed

Lines changed: 273 additions & 63 deletions

File tree

test/common/wpt.js

Lines changed: 130 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class WPTReport {
108108
* @returns {ReportResult}
109109
*/
110110
getResult(spec) {
111-
const name = `/${spec.getRelativePath()}${spec.variant}`;
111+
const name = `/${spec.getTestPath()}`;
112112
if (this.results.has(name)) {
113113
return this.results.get(name);
114114
}
@@ -122,13 +122,7 @@ class WPTReport {
122122
*/
123123
write() {
124124
this.time_end = Date.now();
125-
const results = Array.from(this.results.values())
126-
.map((result) => {
127-
const url = new URL(result.test, 'http://wpt');
128-
url.pathname = url.pathname.replace(/\.js$/, '.html');
129-
result.test = url.href.slice(url.origin.length);
130-
return result;
131-
});
125+
const results = Array.from(this.results.values());
132126

133127
/**
134128
* Return required and some optional properties
@@ -323,18 +317,21 @@ class WPTTestSpec {
323317
* 'test.any.js'
324318
* @param {StatusRule[]} rules
325319
* @param {string} variant test file variant
320+
* @param {'window'|'dedicatedworker'} [globalScope] generated test global
326321
*/
327-
constructor(mod, filename, rules, variant = '') {
322+
constructor(mod, filename, rules, variant = '', globalScope) {
328323
this.module = mod;
329324
this.filename = filename;
330325
this.variant = variant;
326+
this.globalScope = globalScope;
327+
this.rules = [...new Set(rules)];
331328

332329
this.requires = new Set();
333330
this.failedTests = [];
334331
this.flakyTests = [];
335332
this.skipReasons = [];
336333
this.skippedTests = [];
337-
for (const item of rules) {
334+
for (const item of this.rules) {
338335
if (item.requires.length) {
339336
for (const req of item.requires) {
340337
this.requires.add(req);
@@ -364,12 +361,63 @@ class WPTTestSpec {
364361
* @param {string} mod
365362
* @param {string} filename
366363
* @param {StatusRule[]} rules
367-
* @returns {ReturnType<WPTTestSpec['getMeta']>[]}
364+
* @param {(spec: WPTTestSpec) => StatusRule[]} [getAdditionalRules]
365+
* @returns {WPTTestSpec[]}
368366
*/
369-
static from(mod, filename, rules) {
367+
static from(mod, filename, rules, getAdditionalRules) {
370368
const spec = new WPTTestSpec(mod, filename, rules);
371369
const meta = spec.getMeta();
372-
return meta.variant?.map((variant) => new WPTTestSpec(mod, filename, rules, variant)) || [spec];
370+
const variants = meta.variant || [''];
371+
const createSpec = (variant, globalScope) => {
372+
let result = new WPTTestSpec(mod, filename, rules, variant, globalScope);
373+
const additionalRules = getAdditionalRules?.(result) || [];
374+
if (additionalRules.length > 0) {
375+
result = new WPTTestSpec(
376+
mod,
377+
filename,
378+
[...new Set([...rules, ...additionalRules])],
379+
variant,
380+
globalScope,
381+
);
382+
}
383+
return result;
384+
};
385+
386+
if (!spec.isAnyTest()) {
387+
return variants.map((variant) => createSpec(variant));
388+
}
389+
390+
const requestedGlobals = meta.global ?
391+
meta.global.split(',').map((item) => item.trim()) :
392+
['window', 'dedicatedworker'];
393+
const supportedGlobals = new Set();
394+
for (const globalScope of requestedGlobals) {
395+
if (globalScope === 'window' || globalScope === 'dedicatedworker') {
396+
supportedGlobals.add(globalScope);
397+
} else if (globalScope === 'worker') {
398+
supportedGlobals.add('dedicatedworker');
399+
}
400+
}
401+
402+
return ['window', 'dedicatedworker']
403+
.filter((globalScope) => supportedGlobals.has(globalScope))
404+
.flatMap((globalScope) => variants.map(
405+
(variant) => createSpec(variant, globalScope)));
406+
}
407+
408+
/**
409+
* @returns {boolean}
410+
*/
411+
isAnyTest() {
412+
return /\.any\.js$/.test(this.filename);
413+
}
414+
415+
/**
416+
* @returns {boolean}
417+
*/
418+
isWebWorkerTest() {
419+
return /\.worker\.js$/.test(this.filename) ||
420+
this.globalScope === 'dedicatedworker';
373421
}
374422

375423
/**
@@ -392,6 +440,22 @@ class WPTTestSpec {
392440
return path.join(this.module, this.filename);
393441
}
394442

443+
getStatusKey() {
444+
if (this.globalScope === 'dedicatedworker') {
445+
return this.filename.replace(/\.any\.js$/, '.any.worker.html');
446+
}
447+
if (this.globalScope === 'window') {
448+
return this.filename.replace(/\.any\.js$/, '.any.html');
449+
}
450+
return this.filename;
451+
}
452+
453+
getTestPath() {
454+
let testPath = path.join(this.module, this.getStatusKey());
455+
testPath = testPath.replace(/\.js$/, '.html');
456+
return `${testPath.split(path.sep).join('/')}${this.variant}`;
457+
}
458+
395459
getAbsolutePath() {
396460
return fixtures.path('wpt', this.getRelativePath());
397461
}
@@ -541,7 +605,15 @@ class StatusLoader {
541605
for (const file of list) {
542606
const relativePath = path.relative(subDir, file);
543607
const match = this.rules.match(relativePath);
544-
this.specs.push(...WPTTestSpec.from(this.path, relativePath, match));
608+
this.specs.push(...WPTTestSpec.from(
609+
this.path,
610+
relativePath,
611+
match,
612+
(spec) => [
613+
...this.rules.match(spec.getStatusKey()),
614+
...this.rules.match(`${spec.getStatusKey()}${spec.variant}`),
615+
],
616+
));
545617
}
546618
}
547619
}
@@ -577,6 +649,31 @@ const limit = (concurrency) => {
577649
return execute;
578650
};
579651

652+
function getUnexpectedPasses(queue, results) {
653+
const specsByRule = new Map();
654+
for (const spec of queue) {
655+
for (const rule of spec.rules) {
656+
if (Array.isArray(rule.fail?.expected)) {
657+
const specs = specsByRule.get(rule) || [];
658+
specs.push(spec);
659+
specsByRule.set(rule, specs);
660+
}
661+
}
662+
}
663+
664+
const unexpectedPasses = [];
665+
for (const [rule, specs] of specsByRule) {
666+
for (const expectedToFail of rule.fail.expected) {
667+
const failed = specs.some((spec) =>
668+
results[spec.getStatusKey()]?.fail?.expected?.includes(expectedToFail));
669+
if (!failed) {
670+
unexpectedPasses.push(`${rule.key}:${expectedToFail}`);
671+
}
672+
}
673+
}
674+
return unexpectedPasses;
675+
}
676+
580677
class WPTRunner {
581678
constructor(path, { concurrency = os.availableParallelism() - 1 || 1 } = {}) {
582679
// RISC-V has very limited virtual address space in the currently common
@@ -608,6 +705,7 @@ class WPTRunner {
608705
this.inProgress = new Set();
609706
this.workers = new Map();
610707
this.unexpectedFailures = [];
708+
this.skippedSpecCount = 0;
611709

612710
this.subtestCounts = { passed: 0, failed: 0, expectedFailures: 0, skipped: 0, unexpectedPasses: 0 };
613711

@@ -645,7 +743,7 @@ class WPTRunner {
645743
* @returns {string}
646744
*/
647745
fullInitScript(spec) {
648-
const url = new URL(`/${spec.getRelativePath().replace(/\.js$/, '.html')}${spec.variant}`, 'http://wpt');
746+
const url = new URL(`/${spec.getTestPath()}`, 'http://wpt');
649747
const title = spec.getMeta().title;
650748
let { initScript } = this;
651749

@@ -704,20 +802,12 @@ class WPTRunner {
704802
const absolutePath = spec.getAbsolutePath();
705803
const relativePath = spec.getRelativePath();
706804
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
707-
// *.worker.js tests are dedicated worker tests by definition.
708-
// Multi-global (*.any.js) tests whose global scopes include a
709-
// dedicated worker but not a window also run inside an actual Web
710-
// Worker, like the .any.worker.html variant generated by the WPT
711-
// server does. Refs:
805+
// *.worker.js tests are dedicated worker tests by definition. Each
806+
// dedicated worker variant generated from a multi-global (*.any.js)
807+
// test also runs inside an actual Web Worker. Refs:
712808
// https://web-platform-tests.org/writing-tests/testharness.html#multi-global-tests
713-
const isAnyTest = /\.any\.js$/.test(spec.filename);
714-
const globalScopes = isAnyTest ?
715-
(meta.global?.split(',').map((s) => s.trim()) ??
716-
['window', 'dedicatedworker']) : [];
717-
const isWebWorkerTest = /\.worker\.js$/.test(spec.filename) ||
718-
(isAnyTest && !globalScopes.includes('window') &&
719-
(globalScopes.includes('worker') ||
720-
globalScopes.includes('dedicatedworker')));
809+
const isAnyTest = spec.isAnyTest();
810+
const isWebWorkerTest = spec.isWebWorkerTest();
721811

722812
// Scripts specified with the `// META: script=` header. For tests
723813
// that run inside a Web Worker they are imported by the worker
@@ -757,6 +847,8 @@ class WPTRunner {
757847
webWorker: isWebWorkerTest ? {
758848
path: absolutePath,
759849
isAnyTest,
850+
initScript: this.initScript,
851+
variant: spec.variant,
760852
scripts: meta.script?.map(
761853
(script) => this.resource.toRealFilePath(relativePath, script),
762854
) ?? [],
@@ -832,45 +924,16 @@ class WPTRunner {
832924

833925
const failures = [];
834926
let expectedFailures = 0;
835-
let skipped = 0;
836927
for (const [key, item] of Object.entries(this.results)) {
837928
if (item.fail?.unexpected) {
838929
failures.push(key);
839930
}
840931
if (item.fail?.expected) {
841932
expectedFailures++;
842933
}
843-
if (item.skip) {
844-
skipped++;
845-
}
846934
}
847935

848-
const unexpectedPasses = [];
849-
for (const specs of queue) {
850-
const key = specs.filename;
851-
852-
// File has no expected failures
853-
if (!specs.failedTests.length) {
854-
continue;
855-
}
856-
857-
// File was (maybe even conditionally) skipped
858-
if (this.results[key]?.skip) {
859-
continue;
860-
}
861-
862-
// Full check: every expected to fail test is present
863-
const _unexpectedPasses = specs.failedTests.filter((expectedToFail) => {
864-
if (specs.flakyTests.includes(expectedToFail)) {
865-
return false;
866-
}
867-
return this.results[key]?.fail?.expected?.includes(expectedToFail) !== true;
868-
});
869-
if (_unexpectedPasses.length) {
870-
unexpectedPasses.push(..._unexpectedPasses.map((name) => `${key}:${name}`));
871-
continue;
872-
}
873-
}
936+
const unexpectedPasses = getUnexpectedPasses(queue, this.results);
874937

875938
// Write the report on clean exit. The report is also written
876939
// incrementally after each spec completes (see completionCallback)
@@ -879,6 +942,7 @@ class WPTRunner {
879942

880943
const p = (n, word, suffix = 's') => `${n} ${word}${n === 1 ? '' : suffix}`;
881944
const ran = queue.length;
945+
const skipped = this.skippedSpecCount;
882946
const total = ran + skipped;
883947
const passed = ran - expectedFailures - failures.length;
884948
const { subtestCounts } = this;
@@ -976,8 +1040,9 @@ class WPTRunner {
9761040
}
9771041

9781042
addTestResult(spec, item) {
979-
let result = this.results[spec.filename];
980-
result ||= this.results[spec.filename] = {};
1043+
const key = spec.getStatusKey();
1044+
let result = this.results[key];
1045+
result ||= this.results[key] = {};
9811046
if (item.status === kSkip) {
9821047
if (item.name) {
9831048
// Subtest-level skip: { filename: { skipTests: [ ... ] } }
@@ -1056,7 +1121,8 @@ class WPTRunner {
10561121

10571122
skip(spec, reasons) {
10581123
const joinedReasons = reasons.join('; ');
1059-
console.log(`[SKIPPED] ${spec.filename}${spec.variant}: ${joinedReasons}`);
1124+
console.log(`[SKIPPED] ${spec.getTestPath()}: ${joinedReasons}`);
1125+
this.skippedSpecCount++;
10601126
this.addTestResult(spec, {
10611127
status: kSkip,
10621128
reason: joinedReasons,
@@ -1065,6 +1131,7 @@ class WPTRunner {
10651131

10661132
buildQueue() {
10671133
const queue = [];
1134+
this.skippedSpecCount = 0;
10681135
let argFilename;
10691136
let argVariant;
10701137
if (process.argv[2]) {
@@ -1105,7 +1172,9 @@ class WPTRunner {
11051172
}
11061173

11071174
module.exports = {
1175+
getUnexpectedPasses,
11081176
harness: harnessMock,
11091177
ResourceLoader,
1178+
WPTTestSpec,
11101179
WPTRunner,
11111180
};

test/common/wpt/webworker.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
// Refs: https://web-platform-tests.org/writing-tests/testharness.html
55

66
const { pathToFileURL } = require('url');
7+
const {
8+
runInThisContext,
9+
constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER },
10+
} = require('vm');
711

812
globalThis.onmessage = ({ data }) => {
913
// Let the test install its own handler.
@@ -12,12 +16,16 @@ globalThis.onmessage = ({ data }) => {
1216
const { ResourceLoader } = require(data.wptRunner);
1317
const resource = new ResourceLoader(data.wptPath);
1418

19+
globalThis.fetch = function fetch(file) {
20+
return resource.readAsFetch(data.testRelativePath, file);
21+
};
22+
1523
// Pretend the worker was served from the URL the WPT server would have
1624
// used
1725
const fakePath = (data.isAnyTest ?
1826
data.testRelativePath.replace(/\.any\.js$/, '.any.worker.js') :
1927
data.testRelativePath).replace(/\\/g, '/');
20-
const fakeURL = new URL(`/${fakePath}`, 'http://wpt');
28+
const fakeURL = new URL(`/${fakePath}${data.variant}`, 'http://wpt');
2129
// eslint-disable-next-line no-undef
2230
const fakeLocation = { __proto__: WorkerLocation.prototype };
2331
for (const key of ['href', 'origin', 'protocol', 'host', 'hostname',
@@ -98,6 +106,15 @@ globalThis.onmessage = ({ data }) => {
98106
isWorker() { return true; },
99107
isShadowRealm() { return false; },
100108
};
109+
}
110+
111+
if (data.initScript) {
112+
runInThisContext(data.initScript, {
113+
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
114+
});
115+
}
116+
117+
if (data.isAnyTest) {
101118
globalThis.importScripts('/resources/testharness.js');
102119
for (const script of data.scripts) {
103120
globalThis.importScripts(pathToFileURL(script).href);

0 commit comments

Comments
 (0)