1'use strict'; 2 3const assert = require('assert'); 4const fixtures = require('../common/fixtures'); 5const fs = require('fs'); 6const fsPromises = fs.promises; 7const path = require('path'); 8const events = require('events'); 9const os = require('os'); 10const { inspect } = require('util'); 11const { Worker } = require('worker_threads'); 12 13function getBrowserProperties() { 14 const { node: version } = process.versions; // e.g. 18.13.0, 20.0.0-nightly202302078e6e215481 15 const release = /^\d+\.\d+\.\d+$/.test(version); 16 const browser = { 17 browser_channel: release ? 'stable' : 'experimental', 18 browser_version: version, 19 }; 20 21 return browser; 22} 23 24/** 25 * Return one of three expected values 26 * https://github.com/web-platform-tests/wpt/blob/1c6ff12/tools/wptrunner/wptrunner/tests/test_update.py#L953-L958 27 */ 28function getOs() { 29 switch (os.type()) { 30 case 'Linux': 31 return 'linux'; 32 case 'Darwin': 33 return 'mac'; 34 case 'Windows_NT': 35 return 'win'; 36 default: 37 throw new Error('Unsupported os.type()'); 38 } 39} 40 41// https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3705 42function sanitizeUnpairedSurrogates(str) { 43 return str.replace( 44 /([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g, 45 function(_, low, prefix, high) { 46 let output = prefix || ''; // Prefix may be undefined 47 const string = low || high; // Only one of these alternates can match 48 for (let i = 0; i < string.length; i++) { 49 output += codeUnitStr(string[i]); 50 } 51 return output; 52 }); 53} 54 55function codeUnitStr(char) { 56 return 'U+' + char.charCodeAt(0).toString(16); 57} 58 59class WPTReport { 60 constructor() { 61 this.results = []; 62 this.time_start = Date.now(); 63 } 64 65 addResult(name, status) { 66 const result = { 67 test: name, 68 status, 69 subtests: [], 70 addSubtest(name, status, message) { 71 const subtest = { 72 status, 73 // https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3722 74 name: sanitizeUnpairedSurrogates(name), 75 }; 76 if (message) { 77 // https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L4506 78 subtest.message = sanitizeUnpairedSurrogates(message); 79 } 80 this.subtests.push(subtest); 81 return subtest; 82 }, 83 }; 84 this.results.push(result); 85 return result; 86 } 87 88 write() { 89 this.time_end = Date.now(); 90 this.results = this.results.filter((result) => { 91 return result.status === 'SKIP' || result.subtests.length !== 0; 92 }).map((result) => { 93 const url = new URL(result.test, 'http://wpt'); 94 url.pathname = url.pathname.replace(/\.js$/, '.html'); 95 result.test = url.href.slice(url.origin.length); 96 return result; 97 }); 98 99 if (fs.existsSync('out/wpt/wptreport.json')) { 100 const prev = JSON.parse(fs.readFileSync('out/wpt/wptreport.json')); 101 this.results = [...prev.results, ...this.results]; 102 this.time_start = prev.time_start; 103 this.time_end = Math.max(this.time_end, prev.time_end); 104 this.run_info = prev.run_info; 105 } else { 106 /** 107 * Return required and some optional properties 108 * https://github.com/web-platform-tests/wpt.fyi/blob/60da175/api/README.md?plain=1#L331-L335 109 */ 110 this.run_info = { 111 product: 'node.js', 112 ...getBrowserProperties(), 113 revision: process.env.WPT_REVISION || 'unknown', 114 os: getOs(), 115 }; 116 } 117 118 fs.writeFileSync('out/wpt/wptreport.json', JSON.stringify(this)); 119 } 120} 121 122// https://github.com/web-platform-tests/wpt/blob/HEAD/resources/testharness.js 123// TODO: get rid of this half-baked harness in favor of the one 124// pulled from WPT 125const harnessMock = { 126 test: (fn, desc) => { 127 try { 128 fn(); 129 } catch (err) { 130 console.error(`In ${desc}:`); 131 throw err; 132 } 133 }, 134 assert_equals: assert.strictEqual, 135 assert_true: (value, message) => assert.strictEqual(value, true, message), 136 assert_false: (value, message) => assert.strictEqual(value, false, message), 137 assert_throws: (code, func, desc) => { 138 assert.throws(func, function(err) { 139 return typeof err === 'object' && 140 'name' in err && 141 err.name.startsWith(code.name); 142 }, desc); 143 }, 144 assert_array_equals: assert.deepStrictEqual, 145 assert_unreached(desc) { 146 assert.fail(`Reached unreachable code: ${desc}`); 147 }, 148}; 149 150class ResourceLoader { 151 constructor(path) { 152 this.path = path; 153 } 154 155 toRealFilePath(from, url) { 156 // We need to patch this to load the WebIDL parser 157 url = url.replace( 158 '/resources/WebIDLParser.js', 159 '/resources/webidl2/lib/webidl2.js', 160 ); 161 const base = path.dirname(from); 162 return url.startsWith('/') ? 163 fixtures.path('wpt', url) : 164 fixtures.path('wpt', base, url); 165 } 166 167 /** 168 * Load a resource in test/fixtures/wpt specified with a URL 169 * @param {string} from the path of the file loading this resource, 170 * relative to the WPT folder. 171 * @param {string} url the url of the resource being loaded. 172 * @param {boolean} asFetch if true, return the resource in a 173 * pseudo-Response object. 174 */ 175 read(from, url, asFetch = true) { 176 const file = this.toRealFilePath(from, url); 177 if (asFetch) { 178 return fsPromises.readFile(file) 179 .then((data) => { 180 return { 181 ok: true, 182 json() { return JSON.parse(data.toString()); }, 183 text() { return data.toString(); }, 184 }; 185 }); 186 } 187 return fs.readFileSync(file, 'utf8'); 188 } 189} 190 191class StatusRule { 192 constructor(key, value, pattern) { 193 this.key = key; 194 this.requires = value.requires || []; 195 this.fail = value.fail; 196 this.skip = value.skip; 197 if (pattern) { 198 this.pattern = this.transformPattern(pattern); 199 } 200 // TODO(joyeecheung): implement this 201 this.scope = value.scope; 202 this.comment = value.comment; 203 } 204 205 /** 206 * Transform a filename pattern into a RegExp 207 * @param {string} pattern 208 * @returns {RegExp} 209 */ 210 transformPattern(pattern) { 211 const result = path.normalize(pattern).replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&'); 212 return new RegExp(result.replace('*', '.*')); 213 } 214} 215 216class StatusRuleSet { 217 constructor() { 218 // We use two sets of rules to speed up matching 219 this.exactMatch = {}; 220 this.patternMatch = []; 221 } 222 223 /** 224 * @param {object} rules 225 */ 226 addRules(rules) { 227 for (const key of Object.keys(rules)) { 228 if (key.includes('*')) { 229 this.patternMatch.push(new StatusRule(key, rules[key], key)); 230 } else { 231 const normalizedPath = path.normalize(key); 232 this.exactMatch[normalizedPath] = new StatusRule(key, rules[key]); 233 } 234 } 235 } 236 237 match(file) { 238 const result = []; 239 const exact = this.exactMatch[file]; 240 if (exact) { 241 result.push(exact); 242 } 243 for (const item of this.patternMatch) { 244 if (item.pattern.test(file)) { 245 result.push(item); 246 } 247 } 248 return result; 249 } 250} 251 252// A specification of WPT test 253class WPTTestSpec { 254 /** 255 * @param {string} mod name of the WPT module, e.g. 256 * 'html/webappapis/microtask-queuing' 257 * @param {string} filename path of the test, relative to mod, e.g. 258 * 'test.any.js' 259 * @param {StatusRule[]} rules 260 */ 261 constructor(mod, filename, rules) { 262 this.module = mod; 263 this.filename = filename; 264 265 this.requires = new Set(); 266 this.failedTests = []; 267 this.flakyTests = []; 268 this.skipReasons = []; 269 for (const item of rules) { 270 if (item.requires.length) { 271 for (const req of item.requires) { 272 this.requires.add(req); 273 } 274 } 275 if (Array.isArray(item.fail?.expected)) { 276 this.failedTests.push(...item.fail.expected); 277 } 278 if (Array.isArray(item.fail?.flaky)) { 279 this.failedTests.push(...item.fail.flaky); 280 this.flakyTests.push(...item.fail.flaky); 281 } 282 if (item.skip) { 283 this.skipReasons.push(item.skip); 284 } 285 } 286 287 this.failedTests = [...new Set(this.failedTests)]; 288 this.flakyTests = [...new Set(this.flakyTests)]; 289 this.skipReasons = [...new Set(this.skipReasons)]; 290 } 291 292 getRelativePath() { 293 return path.join(this.module, this.filename); 294 } 295 296 getAbsolutePath() { 297 return fixtures.path('wpt', this.getRelativePath()); 298 } 299 300 getContent() { 301 return fs.readFileSync(this.getAbsolutePath(), 'utf8'); 302 } 303} 304 305const kIntlRequirement = { 306 none: 0, 307 small: 1, 308 full: 2, 309 // TODO(joyeecheung): we may need to deal with --with-intl=system-icu 310}; 311 312class IntlRequirement { 313 constructor() { 314 this.currentIntl = kIntlRequirement.none; 315 if (process.config.variables.v8_enable_i18n_support === 0) { 316 this.currentIntl = kIntlRequirement.none; 317 return; 318 } 319 // i18n enabled 320 if (process.config.variables.icu_small) { 321 this.currentIntl = kIntlRequirement.small; 322 } else { 323 this.currentIntl = kIntlRequirement.full; 324 } 325 } 326 327 /** 328 * @param {Set} requires 329 * @returns {string|false} The config that the build is lacking, or false 330 */ 331 isLacking(requires) { 332 const current = this.currentIntl; 333 if (requires.has('full-icu') && current !== kIntlRequirement.full) { 334 return 'full-icu'; 335 } 336 if (requires.has('small-icu') && current < kIntlRequirement.small) { 337 return 'small-icu'; 338 } 339 return false; 340 } 341} 342 343const intlRequirements = new IntlRequirement(); 344 345class StatusLoader { 346 /** 347 * @param {string} path relative path of the WPT subset 348 */ 349 constructor(path) { 350 this.path = path; 351 this.loaded = false; 352 this.rules = new StatusRuleSet(); 353 /** @type {WPTTestSpec[]} */ 354 this.specs = []; 355 } 356 357 /** 358 * Grep for all .*.js file recursively in a directory. 359 * @param {string} dir 360 */ 361 grep(dir) { 362 let result = []; 363 const list = fs.readdirSync(dir); 364 for (const file of list) { 365 const filepath = path.join(dir, file); 366 const stat = fs.statSync(filepath); 367 if (stat.isDirectory()) { 368 const list = this.grep(filepath); 369 result = result.concat(list); 370 } else { 371 if (!(/\.\w+\.js$/.test(filepath)) || filepath.endsWith('.helper.js')) { 372 continue; 373 } 374 result.push(filepath); 375 } 376 } 377 return result; 378 } 379 380 load() { 381 const dir = path.join(__dirname, '..', 'wpt'); 382 const statusFile = path.join(dir, 'status', `${this.path}.json`); 383 const result = JSON.parse(fs.readFileSync(statusFile, 'utf8')); 384 this.rules.addRules(result); 385 386 const subDir = fixtures.path('wpt', this.path); 387 const list = this.grep(subDir); 388 for (const file of list) { 389 const relativePath = path.relative(subDir, file); 390 const match = this.rules.match(relativePath); 391 this.specs.push(new WPTTestSpec(this.path, relativePath, match)); 392 } 393 this.loaded = true; 394 } 395} 396 397const kPass = 'pass'; 398const kFail = 'fail'; 399const kSkip = 'skip'; 400const kTimeout = 'timeout'; 401const kIncomplete = 'incomplete'; 402const kUncaught = 'uncaught'; 403const NODE_UNCAUGHT = 100; 404 405class WPTRunner { 406 constructor(path) { 407 this.path = path; 408 this.resource = new ResourceLoader(path); 409 410 this.flags = []; 411 this.dummyGlobalThisScript = null; 412 this.initScript = null; 413 414 this.status = new StatusLoader(path); 415 this.status.load(); 416 this.specMap = new Map( 417 this.status.specs.map((item) => [item.filename, item]), 418 ); 419 420 this.results = {}; 421 this.inProgress = new Set(); 422 this.workers = new Map(); 423 this.unexpectedFailures = []; 424 425 this.scriptsModifier = null; 426 427 if (process.env.WPT_REPORT != null) { 428 this.report = new WPTReport(); 429 } 430 } 431 432 /** 433 * Sets the Node.js flags passed to the worker. 434 * @param {Array<string>} flags 435 */ 436 setFlags(flags) { 437 this.flags = flags; 438 } 439 440 /** 441 * Sets a script to be run in the worker before executing the tests. 442 * @param {string} script 443 */ 444 setInitScript(script) { 445 this.initScript = script; 446 } 447 448 /** 449 * Set the scripts modifier for each script. 450 * @param {(meta: { code: string, filename: string }) => void} modifier 451 */ 452 setScriptModifier(modifier) { 453 this.scriptsModifier = modifier; 454 } 455 456 fullInitScript(hasSubsetScript, locationSearchString) { 457 let { initScript } = this; 458 if (hasSubsetScript || locationSearchString) { 459 initScript = `${initScript}\n\n//===\nglobalThis.location ||= {};`; 460 } 461 462 if (locationSearchString) { 463 initScript = `${initScript}\n\n//===\nglobalThis.location.search = "${locationSearchString}";`; 464 } 465 466 if (initScript === null && this.dummyGlobalThisScript === null) { 467 return null; 468 } 469 470 if (initScript === null) { 471 return this.dummyGlobalThisScript; 472 } else if (this.dummyGlobalThisScript === null) { 473 return initScript; 474 } 475 476 return `${this.dummyGlobalThisScript}\n\n//===\n${initScript}`; 477 } 478 479 /** 480 * Pretend the runner is run in `name`'s environment (globalThis). 481 * @param {'Window'} name 482 * @see {@link https://github.com/nodejs/node/blob/24673ace8ae196bd1c6d4676507d6e8c94cf0b90/test/fixtures/wpt/resources/idlharness.js#L654-L671} 483 */ 484 pretendGlobalThisAs(name) { 485 switch (name) { 486 case 'Window': { 487 this.dummyGlobalThisScript = 488 'global.Window = Object.getPrototypeOf(globalThis).constructor;'; 489 break; 490 } 491 492 // TODO(XadillaX): implement `ServiceWorkerGlobalScope`, 493 // `DedicateWorkerGlobalScope`, etc. 494 // 495 // e.g. `ServiceWorkerGlobalScope` should implement dummy 496 // `addEventListener` and so on. 497 498 default: throw new Error(`Invalid globalThis type ${name}.`); 499 } 500 } 501 502 // TODO(joyeecheung): work with the upstream to port more tests in .html 503 // to .js. 504 async runJsTests() { 505 let queue = []; 506 507 // If the tests are run as `node test/wpt/test-something.js subset.any.js`, 508 // only `subset.any.js` will be run by the runner. 509 if (process.argv[2]) { 510 const filename = process.argv[2]; 511 if (!this.specMap.has(filename)) { 512 throw new Error(`${filename} not found!`); 513 } 514 queue.push(this.specMap.get(filename)); 515 } else { 516 queue = this.buildQueue(); 517 } 518 519 this.inProgress = new Set(queue.map((spec) => spec.filename)); 520 521 for (const spec of queue) { 522 const testFileName = spec.filename; 523 const content = spec.getContent(); 524 const meta = spec.meta = this.getMeta(content); 525 526 const absolutePath = spec.getAbsolutePath(); 527 const relativePath = spec.getRelativePath(); 528 const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js'); 529 const scriptsToRun = []; 530 let hasSubsetScript = false; 531 532 // Scripts specified with the `// META: script=` header 533 if (meta.script) { 534 for (const script of meta.script) { 535 if (script === '/common/subset-tests.js' || script === '/common/subset-tests-by-key.js') { 536 hasSubsetScript = true; 537 } 538 const obj = { 539 filename: this.resource.toRealFilePath(relativePath, script), 540 code: this.resource.read(relativePath, script, false), 541 }; 542 this.scriptsModifier?.(obj); 543 scriptsToRun.push(obj); 544 } 545 } 546 // The actual test 547 const obj = { 548 code: content, 549 filename: absolutePath, 550 }; 551 this.scriptsModifier?.(obj); 552 scriptsToRun.push(obj); 553 554 /** 555 * Example test with no META variant 556 * https://github.com/nodejs/node/blob/03854f6/test/fixtures/wpt/WebCryptoAPI/sign_verify/hmac.https.any.js#L1-L4 557 * 558 * Example test with multiple META variants 559 * https://github.com/nodejs/node/blob/03854f6/test/fixtures/wpt/WebCryptoAPI/generateKey/successes_RSASSA-PKCS1-v1_5.https.any.js#L1-L9 560 */ 561 for (const variant of meta.variant || ['']) { 562 const workerPath = path.join(__dirname, 'wpt/worker.js'); 563 const worker = new Worker(workerPath, { 564 execArgv: this.flags, 565 workerData: { 566 testRelativePath: relativePath, 567 wptRunner: __filename, 568 wptPath: this.path, 569 initScript: this.fullInitScript(hasSubsetScript, variant), 570 harness: { 571 code: fs.readFileSync(harnessPath, 'utf8'), 572 filename: harnessPath, 573 }, 574 scriptsToRun, 575 }, 576 }); 577 this.workers.set(testFileName, worker); 578 579 let reportResult; 580 worker.on('message', (message) => { 581 switch (message.type) { 582 case 'result': 583 reportResult ||= this.report?.addResult(`/${relativePath}${variant}`, 'OK'); 584 return this.resultCallback(testFileName, message.result, reportResult); 585 case 'completion': 586 return this.completionCallback(testFileName, message.status); 587 default: 588 throw new Error(`Unexpected message from worker: ${message.type}`); 589 } 590 }); 591 592 worker.on('error', (err) => { 593 if (!this.inProgress.has(testFileName)) { 594 // The test is already finished. Ignore errors that occur after it. 595 // This can happen normally, for example in timers tests. 596 return; 597 } 598 this.fail( 599 testFileName, 600 { 601 status: NODE_UNCAUGHT, 602 name: 'evaluation in WPTRunner.runJsTests()', 603 message: err.message, 604 stack: inspect(err), 605 }, 606 kUncaught, 607 ); 608 this.inProgress.delete(testFileName); 609 }); 610 611 await events.once(worker, 'exit').catch(() => {}); 612 } 613 } 614 615 process.on('exit', () => { 616 for (const spec of this.inProgress) { 617 this.fail(spec, { name: 'Incomplete' }, kIncomplete); 618 } 619 inspect.defaultOptions.depth = Infinity; 620 // Sorts the rules to have consistent output 621 console.log(JSON.stringify(Object.keys(this.results).sort().reduce( 622 (obj, key) => { 623 obj[key] = this.results[key]; 624 return obj; 625 }, 626 {}, 627 ), null, 2)); 628 629 const failures = []; 630 let expectedFailures = 0; 631 let skipped = 0; 632 for (const [key, item] of Object.entries(this.results)) { 633 if (item.fail?.unexpected) { 634 failures.push(key); 635 } 636 if (item.fail?.expected) { 637 expectedFailures++; 638 } 639 if (item.skip) { 640 skipped++; 641 } 642 } 643 644 const unexpectedPasses = []; 645 for (const specMap of queue) { 646 const key = specMap.filename; 647 648 // File has no expected failures 649 if (!specMap.failedTests.length) { 650 continue; 651 } 652 653 // File was (maybe even conditionally) skipped 654 if (this.results[key]?.skip) { 655 continue; 656 } 657 658 // Full check: every expected to fail test is present 659 if (specMap.failedTests.some((expectedToFail) => { 660 if (specMap.flakyTests.includes(expectedToFail)) { 661 return false; 662 } 663 return this.results[key]?.fail?.expected?.includes(expectedToFail) !== true; 664 })) { 665 unexpectedPasses.push(key); 666 continue; 667 } 668 } 669 670 this.report?.write(); 671 672 const ran = queue.length; 673 const total = ran + skipped; 674 const passed = ran - expectedFailures - failures.length; 675 console.log(`Ran ${ran}/${total} tests, ${skipped} skipped,`, 676 `${passed} passed, ${expectedFailures} expected failures,`, 677 `${failures.length} unexpected failures,`, 678 `${unexpectedPasses.length} unexpected passes`); 679 if (failures.length > 0) { 680 const file = path.join('test', 'wpt', 'status', `${this.path}.json`); 681 throw new Error( 682 `Found ${failures.length} unexpected failures. ` + 683 `Consider updating ${file} for these files:\n${failures.join('\n')}`); 684 } 685 if (unexpectedPasses.length > 0) { 686 const file = path.join('test', 'wpt', 'status', `${this.path}.json`); 687 throw new Error( 688 `Found ${unexpectedPasses.length} unexpected passes. ` + 689 `Consider updating ${file} for these files:\n${unexpectedPasses.join('\n')}`); 690 } 691 }); 692 } 693 694 getTestTitle(filename) { 695 const spec = this.specMap.get(filename); 696 return spec.meta?.title || filename.split('.')[0]; 697 } 698 699 // Map WPT test status to strings 700 getTestStatus(status) { 701 switch (status) { 702 case 1: 703 return kFail; 704 case 2: 705 return kTimeout; 706 case 3: 707 return kIncomplete; 708 case NODE_UNCAUGHT: 709 return kUncaught; 710 default: 711 return kPass; 712 } 713 } 714 715 /** 716 * Report the status of each specific test case (there could be multiple 717 * in one test file). 718 * @param {string} filename 719 * @param {Test} test The Test object returned by WPT harness 720 */ 721 resultCallback(filename, test, reportResult) { 722 const status = this.getTestStatus(test.status); 723 const title = this.getTestTitle(filename); 724 if (/^Untitled( \d+)?$/.test(test.name)) { 725 test.name = `${title}${test.name.slice(8)}`; 726 } 727 console.log(`---- ${title} ----`); 728 if (status !== kPass) { 729 this.fail(filename, test, status, reportResult); 730 } else { 731 this.succeed(filename, test, status, reportResult); 732 } 733 } 734 735 /** 736 * Report the status of each WPT test (one per file) 737 * @param {string} filename 738 * @param {object} harnessStatus - The status object returned by WPT harness. 739 */ 740 completionCallback(filename, harnessStatus) { 741 const status = this.getTestStatus(harnessStatus.status); 742 743 // Treat it like a test case failure 744 if (status === kTimeout) { 745 this.fail(filename, { name: 'WPT testharness timeout' }, kTimeout); 746 } 747 this.inProgress.delete(filename); 748 // Always force termination of the worker. Some tests allocate resources 749 // that would otherwise keep it alive. 750 this.workers.get(filename).terminate(); 751 } 752 753 addTestResult(filename, item) { 754 let result = this.results[filename]; 755 if (!result) { 756 result = this.results[filename] = {}; 757 } 758 if (item.status === kSkip) { 759 // { filename: { skip: 'reason' } } 760 result[kSkip] = item.reason; 761 } else { 762 // { filename: { fail: { expected: [ ... ], 763 // unexpected: [ ... ] } }} 764 if (!result[item.status]) { 765 result[item.status] = {}; 766 } 767 const key = item.expected ? 'expected' : 'unexpected'; 768 if (!result[item.status][key]) { 769 result[item.status][key] = []; 770 } 771 const hasName = result[item.status][key].includes(item.name); 772 if (!hasName) { 773 result[item.status][key].push(item.name); 774 } 775 } 776 } 777 778 succeed(filename, test, status, reportResult) { 779 console.log(`[${status.toUpperCase()}] ${test.name}`); 780 reportResult?.addSubtest(test.name, 'PASS'); 781 } 782 783 fail(filename, test, status, reportResult) { 784 const spec = this.specMap.get(filename); 785 const expected = spec.failedTests.includes(test.name); 786 if (expected) { 787 console.log(`[EXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`); 788 console.log(test.message || status); 789 } else { 790 console.log(`[UNEXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`); 791 } 792 if (status === kFail || status === kUncaught) { 793 console.log(test.message); 794 console.log(test.stack); 795 } 796 const command = `${process.execPath} ${process.execArgv}` + 797 ` ${require.main.filename} ${filename}`; 798 console.log(`Command: ${command}\n`); 799 800 reportResult?.addSubtest(test.name, 'FAIL', test.message); 801 802 this.addTestResult(filename, { 803 name: test.name, 804 expected, 805 status: kFail, 806 reason: test.message || status, 807 }); 808 } 809 810 skip(filename, reasons) { 811 const title = this.getTestTitle(filename); 812 console.log(`---- ${title} ----`); 813 const joinedReasons = reasons.join('; '); 814 console.log(`[SKIPPED] ${joinedReasons}`); 815 this.addTestResult(filename, { 816 status: kSkip, 817 reason: joinedReasons, 818 }); 819 } 820 821 getMeta(code) { 822 const matches = code.match(/\/\/ META: .+/g); 823 if (!matches) { 824 return {}; 825 } 826 const result = {}; 827 for (const match of matches) { 828 const parts = match.match(/\/\/ META: ([^=]+?)=(.+)/); 829 const key = parts[1]; 830 const value = parts[2]; 831 if (key === 'script' || key === 'variant') { 832 if (result[key]) { 833 result[key].push(value); 834 } else { 835 result[key] = [value]; 836 } 837 } else { 838 result[key] = value; 839 } 840 } 841 return result; 842 } 843 844 buildQueue() { 845 const queue = []; 846 for (const spec of this.specMap.values()) { 847 const filename = spec.filename; 848 if (spec.skipReasons.length > 0) { 849 this.skip(filename, spec.skipReasons); 850 continue; 851 } 852 853 const lackingIntl = intlRequirements.isLacking(spec.requires); 854 if (lackingIntl) { 855 this.skip(filename, [ `requires ${lackingIntl}` ]); 856 continue; 857 } 858 859 queue.push(spec); 860 } 861 return queue; 862 } 863} 864 865module.exports = { 866 harness: harnessMock, 867 ResourceLoader, 868 WPTRunner, 869}; 870