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.globalThisInitScripts = []; 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 (this.globalThisInitScripts.length === null) { 467 return initScript; 468 } 469 470 const globalThisInitScript = this.globalThisInitScripts.join('\n\n//===\n'); 471 472 if (initScript === null) { 473 return globalThisInitScript; 474 } 475 476 return `${globalThisInitScript}\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.globalThisInitScripts.push( 488 `global.Window = Object.getPrototypeOf(globalThis).constructor; 489 self.GLOBAL.isWorker = () => false;`); 490 this.loadLazyGlobals(); 491 break; 492 } 493 494 // TODO(XadillaX): implement `ServiceWorkerGlobalScope`, 495 // `DedicateWorkerGlobalScope`, etc. 496 // 497 // e.g. `ServiceWorkerGlobalScope` should implement dummy 498 // `addEventListener` and so on. 499 500 default: throw new Error(`Invalid globalThis type ${name}.`); 501 } 502 } 503 504 loadLazyGlobals() { 505 const lazyProperties = [ 506 'DOMException', 507 'Performance', 'PerformanceEntry', 'PerformanceMark', 'PerformanceMeasure', 508 'PerformanceObserver', 'PerformanceObserverEntryList', 'PerformanceResourceTiming', 509 'Blob', 'atob', 'btoa', 510 'MessageChannel', 'MessagePort', 'MessageEvent', 511 'EventTarget', 'Event', 512 'AbortController', 'AbortSignal', 513 'performance', 514 'TransformStream', 'TransformStreamDefaultController', 515 'WritableStream', 'WritableStreamDefaultController', 'WritableStreamDefaultWriter', 516 'ReadableStream', 'ReadableStreamDefaultReader', 517 'ReadableStreamBYOBReader', 'ReadableStreamBYOBRequest', 518 'ReadableByteStreamController', 'ReadableStreamDefaultController', 519 'ByteLengthQueuingStrategy', 'CountQueuingStrategy', 520 'TextEncoderStream', 'TextDecoderStream', 521 'CompressionStream', 'DecompressionStream', 522 ]; 523 if (Boolean(process.versions.openssl) && !process.env.NODE_SKIP_CRYPTO) { 524 lazyProperties.push('crypto'); 525 } 526 const script = lazyProperties.map((name) => `globalThis.${name};`).join('\n'); 527 this.globalThisInitScripts.push(script); 528 } 529 530 // TODO(joyeecheung): work with the upstream to port more tests in .html 531 // to .js. 532 async runJsTests() { 533 let queue = []; 534 535 // If the tests are run as `node test/wpt/test-something.js subset.any.js`, 536 // only `subset.any.js` will be run by the runner. 537 if (process.argv[2]) { 538 const filename = process.argv[2]; 539 if (!this.specMap.has(filename)) { 540 throw new Error(`${filename} not found!`); 541 } 542 queue.push(this.specMap.get(filename)); 543 } else { 544 queue = this.buildQueue(); 545 } 546 547 this.inProgress = new Set(queue.map((spec) => spec.filename)); 548 549 for (const spec of queue) { 550 const testFileName = spec.filename; 551 const content = spec.getContent(); 552 const meta = spec.meta = this.getMeta(content); 553 554 const absolutePath = spec.getAbsolutePath(); 555 const relativePath = spec.getRelativePath(); 556 const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js'); 557 const scriptsToRun = []; 558 let hasSubsetScript = false; 559 560 // Scripts specified with the `// META: script=` header 561 if (meta.script) { 562 for (const script of meta.script) { 563 if (script === '/common/subset-tests.js' || script === '/common/subset-tests-by-key.js') { 564 hasSubsetScript = true; 565 } 566 const obj = { 567 filename: this.resource.toRealFilePath(relativePath, script), 568 code: this.resource.read(relativePath, script, false), 569 }; 570 this.scriptsModifier?.(obj); 571 scriptsToRun.push(obj); 572 } 573 } 574 // The actual test 575 const obj = { 576 code: content, 577 filename: absolutePath, 578 }; 579 this.scriptsModifier?.(obj); 580 scriptsToRun.push(obj); 581 582 /** 583 * Example test with no META variant 584 * https://github.com/nodejs/node/blob/03854f6/test/fixtures/wpt/WebCryptoAPI/sign_verify/hmac.https.any.js#L1-L4 585 * 586 * Example test with multiple META variants 587 * https://github.com/nodejs/node/blob/03854f6/test/fixtures/wpt/WebCryptoAPI/generateKey/successes_RSASSA-PKCS1-v1_5.https.any.js#L1-L9 588 */ 589 for (const variant of meta.variant || ['']) { 590 const workerPath = path.join(__dirname, 'wpt/worker.js'); 591 const worker = new Worker(workerPath, { 592 execArgv: this.flags, 593 workerData: { 594 testRelativePath: relativePath, 595 wptRunner: __filename, 596 wptPath: this.path, 597 initScript: this.fullInitScript(hasSubsetScript, variant), 598 harness: { 599 code: fs.readFileSync(harnessPath, 'utf8'), 600 filename: harnessPath, 601 }, 602 scriptsToRun, 603 }, 604 }); 605 this.workers.set(testFileName, worker); 606 607 let reportResult; 608 worker.on('message', (message) => { 609 switch (message.type) { 610 case 'result': 611 reportResult ||= this.report?.addResult(`/${relativePath}${variant}`, 'OK'); 612 return this.resultCallback(testFileName, message.result, reportResult); 613 case 'completion': 614 return this.completionCallback(testFileName, message.status); 615 default: 616 throw new Error(`Unexpected message from worker: ${message.type}`); 617 } 618 }); 619 620 worker.on('error', (err) => { 621 if (!this.inProgress.has(testFileName)) { 622 // The test is already finished. Ignore errors that occur after it. 623 // This can happen normally, for example in timers tests. 624 return; 625 } 626 this.fail( 627 testFileName, 628 { 629 status: NODE_UNCAUGHT, 630 name: 'evaluation in WPTRunner.runJsTests()', 631 message: err.message, 632 stack: inspect(err), 633 }, 634 kUncaught, 635 ); 636 this.inProgress.delete(testFileName); 637 }); 638 639 await events.once(worker, 'exit').catch(() => {}); 640 } 641 } 642 643 process.on('exit', () => { 644 for (const spec of this.inProgress) { 645 this.fail(spec, { name: 'Incomplete' }, kIncomplete); 646 } 647 inspect.defaultOptions.depth = Infinity; 648 // Sorts the rules to have consistent output 649 console.log(JSON.stringify(Object.keys(this.results).sort().reduce( 650 (obj, key) => { 651 obj[key] = this.results[key]; 652 return obj; 653 }, 654 {}, 655 ), null, 2)); 656 657 const failures = []; 658 let expectedFailures = 0; 659 let skipped = 0; 660 for (const [key, item] of Object.entries(this.results)) { 661 if (item.fail?.unexpected) { 662 failures.push(key); 663 } 664 if (item.fail?.expected) { 665 expectedFailures++; 666 } 667 if (item.skip) { 668 skipped++; 669 } 670 } 671 672 const unexpectedPasses = []; 673 for (const specMap of queue) { 674 const key = specMap.filename; 675 676 // File has no expected failures 677 if (!specMap.failedTests.length) { 678 continue; 679 } 680 681 // File was (maybe even conditionally) skipped 682 if (this.results[key]?.skip) { 683 continue; 684 } 685 686 // Full check: every expected to fail test is present 687 if (specMap.failedTests.some((expectedToFail) => { 688 if (specMap.flakyTests.includes(expectedToFail)) { 689 return false; 690 } 691 return this.results[key]?.fail?.expected?.includes(expectedToFail) !== true; 692 })) { 693 unexpectedPasses.push(key); 694 continue; 695 } 696 } 697 698 this.report?.write(); 699 700 const ran = queue.length; 701 const total = ran + skipped; 702 const passed = ran - expectedFailures - failures.length; 703 console.log(`Ran ${ran}/${total} tests, ${skipped} skipped,`, 704 `${passed} passed, ${expectedFailures} expected failures,`, 705 `${failures.length} unexpected failures,`, 706 `${unexpectedPasses.length} unexpected passes`); 707 if (failures.length > 0) { 708 const file = path.join('test', 'wpt', 'status', `${this.path}.json`); 709 throw new Error( 710 `Found ${failures.length} unexpected failures. ` + 711 `Consider updating ${file} for these files:\n${failures.join('\n')}`); 712 } 713 if (unexpectedPasses.length > 0) { 714 const file = path.join('test', 'wpt', 'status', `${this.path}.json`); 715 throw new Error( 716 `Found ${unexpectedPasses.length} unexpected passes. ` + 717 `Consider updating ${file} for these files:\n${unexpectedPasses.join('\n')}`); 718 } 719 }); 720 } 721 722 getTestTitle(filename) { 723 const spec = this.specMap.get(filename); 724 return spec.meta?.title || filename.split('.')[0]; 725 } 726 727 // Map WPT test status to strings 728 getTestStatus(status) { 729 switch (status) { 730 case 1: 731 return kFail; 732 case 2: 733 return kTimeout; 734 case 3: 735 return kIncomplete; 736 case NODE_UNCAUGHT: 737 return kUncaught; 738 default: 739 return kPass; 740 } 741 } 742 743 /** 744 * Report the status of each specific test case (there could be multiple 745 * in one test file). 746 * @param {string} filename 747 * @param {Test} test The Test object returned by WPT harness 748 */ 749 resultCallback(filename, test, reportResult) { 750 const status = this.getTestStatus(test.status); 751 const title = this.getTestTitle(filename); 752 if (/^Untitled( \d+)?$/.test(test.name)) { 753 test.name = `${title}${test.name.slice(8)}`; 754 } 755 console.log(`---- ${title} ----`); 756 if (status !== kPass) { 757 this.fail(filename, test, status, reportResult); 758 } else { 759 this.succeed(filename, test, status, reportResult); 760 } 761 } 762 763 /** 764 * Report the status of each WPT test (one per file) 765 * @param {string} filename 766 * @param {object} harnessStatus - The status object returned by WPT harness. 767 */ 768 completionCallback(filename, harnessStatus) { 769 const status = this.getTestStatus(harnessStatus.status); 770 771 // Treat it like a test case failure 772 if (status === kTimeout) { 773 this.fail(filename, { name: 'WPT testharness timeout' }, kTimeout); 774 } 775 this.inProgress.delete(filename); 776 // Always force termination of the worker. Some tests allocate resources 777 // that would otherwise keep it alive. 778 this.workers.get(filename).terminate(); 779 } 780 781 addTestResult(filename, item) { 782 let result = this.results[filename]; 783 if (!result) { 784 result = this.results[filename] = {}; 785 } 786 if (item.status === kSkip) { 787 // { filename: { skip: 'reason' } } 788 result[kSkip] = item.reason; 789 } else { 790 // { filename: { fail: { expected: [ ... ], 791 // unexpected: [ ... ] } }} 792 if (!result[item.status]) { 793 result[item.status] = {}; 794 } 795 const key = item.expected ? 'expected' : 'unexpected'; 796 if (!result[item.status][key]) { 797 result[item.status][key] = []; 798 } 799 const hasName = result[item.status][key].includes(item.name); 800 if (!hasName) { 801 result[item.status][key].push(item.name); 802 } 803 } 804 } 805 806 succeed(filename, test, status, reportResult) { 807 console.log(`[${status.toUpperCase()}] ${test.name}`); 808 reportResult?.addSubtest(test.name, 'PASS'); 809 } 810 811 fail(filename, test, status, reportResult) { 812 const spec = this.specMap.get(filename); 813 const expected = spec.failedTests.includes(test.name); 814 if (expected) { 815 console.log(`[EXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`); 816 console.log(test.message || status); 817 } else { 818 console.log(`[UNEXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`); 819 } 820 if (status === kFail || status === kUncaught) { 821 console.log(test.message); 822 console.log(test.stack); 823 } 824 const command = `${process.execPath} ${process.execArgv}` + 825 ` ${require.main.filename} ${filename}`; 826 console.log(`Command: ${command}\n`); 827 828 reportResult?.addSubtest(test.name, 'FAIL', test.message); 829 830 this.addTestResult(filename, { 831 name: test.name, 832 expected, 833 status: kFail, 834 reason: test.message || status, 835 }); 836 } 837 838 skip(filename, reasons) { 839 const title = this.getTestTitle(filename); 840 console.log(`---- ${title} ----`); 841 const joinedReasons = reasons.join('; '); 842 console.log(`[SKIPPED] ${joinedReasons}`); 843 this.addTestResult(filename, { 844 status: kSkip, 845 reason: joinedReasons, 846 }); 847 } 848 849 getMeta(code) { 850 const matches = code.match(/\/\/ META: .+/g); 851 if (!matches) { 852 return {}; 853 } 854 const result = {}; 855 for (const match of matches) { 856 const parts = match.match(/\/\/ META: ([^=]+?)=(.+)/); 857 const key = parts[1]; 858 const value = parts[2]; 859 if (key === 'script' || key === 'variant') { 860 if (result[key]) { 861 result[key].push(value); 862 } else { 863 result[key] = [value]; 864 } 865 } else { 866 result[key] = value; 867 } 868 } 869 return result; 870 } 871 872 buildQueue() { 873 const queue = []; 874 for (const spec of this.specMap.values()) { 875 const filename = spec.filename; 876 if (spec.skipReasons.length > 0) { 877 this.skip(filename, spec.skipReasons); 878 continue; 879 } 880 881 const lackingIntl = intlRequirements.isLacking(spec.requires); 882 if (lackingIntl) { 883 this.skip(filename, [ `requires ${lackingIntl}` ]); 884 continue; 885 } 886 887 queue.push(spec); 888 } 889 return queue; 890 } 891} 892 893module.exports = { 894 harness: harnessMock, 895 ResourceLoader, 896 WPTRunner, 897}; 898