%PDF- %PDF-
Direktori : /home/vacivi36/vittasync.vacivitta.com.br/vittasync/node/test/common/ |
Current File : /home/vacivi36/vittasync.vacivitta.com.br/vittasync/node/test/common/wpt.js |
'use strict'; const assert = require('assert'); const fixtures = require('../common/fixtures'); const fs = require('fs'); const fsPromises = fs.promises; const path = require('path'); const events = require('events'); const os = require('os'); const { inspect } = require('util'); const { Worker } = require('worker_threads'); const workerPath = path.join(__dirname, 'wpt/worker.js'); function getBrowserProperties() { const { node: version } = process.versions; // e.g. 18.13.0, 20.0.0-nightly202302078e6e215481 const release = /^\d+\.\d+\.\d+$/.test(version); const browser = { browser_channel: release ? 'stable' : 'experimental', browser_version: version, }; return browser; } /** * Return one of three expected values * https://github.com/web-platform-tests/wpt/blob/1c6ff12/tools/wptrunner/wptrunner/tests/test_update.py#L953-L958 */ function getOs() { switch (os.type()) { case 'Linux': return 'linux'; case 'Darwin': return 'mac'; case 'Windows_NT': return 'win'; default: throw new Error('Unsupported os.type()'); } } // https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3705 function sanitizeUnpairedSurrogates(str) { return str.replace( /([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g, function(_, low, prefix, high) { let output = prefix || ''; // Prefix may be undefined const string = low || high; // Only one of these alternates can match for (let i = 0; i < string.length; i++) { output += codeUnitStr(string[i]); } return output; }); } function codeUnitStr(char) { return 'U+' + char.charCodeAt(0).toString(16); } class ReportResult { #startTime; constructor(name) { this.test = name; this.status = 'OK'; this.subtests = []; this.#startTime = Date.now(); } addSubtest(name, status, message) { const subtest = { status, // https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3722 name: sanitizeUnpairedSurrogates(name), }; if (message) { // https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L4506 subtest.message = sanitizeUnpairedSurrogates(message); } this.subtests.push(subtest); return subtest; } finish(status) { this.status = status ?? 'OK'; this.duration = Date.now() - this.#startTime; } } // Generates a report that can be uploaded to wpt.fyi. // Checkout https://github.com/web-platform-tests/wpt.fyi/tree/main/api#results-creation // for more details. class WPTReport { constructor(path) { this.filename = `report-${path.replaceAll('/', '-')}.json`; /** @type {Map<string, ReportResult>} */ this.results = new Map(); this.time_start = Date.now(); } /** * Get or create a ReportResult for a test spec. * @param {WPTTestSpec} spec */ getResult(spec) { const name = `/${spec.getRelativePath()}${spec.variant}`; if (this.results.has(name)) { return this.results.get(name); } const result = new ReportResult(name); this.results.set(name, result); return result; } write() { this.time_end = Date.now(); const results = Array.from(this.results.values()) .map((result) => { const url = new URL(result.test, 'http://wpt'); url.pathname = url.pathname.replace(/\.js$/, '.html'); result.test = url.href.slice(url.origin.length); return result; }); /** * Return required and some optional properties * https://github.com/web-platform-tests/wpt.fyi/blob/60da175/api/README.md?plain=1#L331-L335 */ this.run_info = { product: 'node.js', ...getBrowserProperties(), revision: process.env.WPT_REVISION || 'unknown', os: getOs(), }; fs.writeFileSync(`out/wpt/${this.filename}`, JSON.stringify({ time_start: this.time_start, time_end: this.time_end, run_info: this.run_info, results: results, })); } } // https://github.com/web-platform-tests/wpt/blob/HEAD/resources/testharness.js // TODO: get rid of this half-baked harness in favor of the one // pulled from WPT const harnessMock = { test: (fn, desc) => { try { fn(); } catch (err) { console.error(`In ${desc}:`); throw err; } }, assert_equals: assert.strictEqual, assert_true: (value, message) => assert.strictEqual(value, true, message), assert_false: (value, message) => assert.strictEqual(value, false, message), assert_throws: (code, func, desc) => { assert.throws(func, function(err) { return typeof err === 'object' && 'name' in err && err.name.startsWith(code.name); }, desc); }, assert_array_equals: assert.deepStrictEqual, assert_unreached(desc) { assert.fail(`Reached unreachable code: ${desc}`); }, }; class ResourceLoader { constructor(path) { this.path = path; } toRealFilePath(from, url) { // We need to patch this to load the WebIDL parser url = url.replace( '/resources/WebIDLParser.js', '/resources/webidl2/lib/webidl2.js', ); const base = path.dirname(from); return url.startsWith('/') ? fixtures.path('wpt', url) : fixtures.path('wpt', base, url); } /** * Load a resource in test/fixtures/wpt specified with a URL * @param {string} from the path of the file loading this resource, * relative to the WPT folder. * @param {string} url the url of the resource being loaded. */ read(from, url) { const file = this.toRealFilePath(from, url); return fs.readFileSync(file, 'utf8'); } /** * Load a resource in test/fixtures/wpt specified with a URL * @param {string} from the path of the file loading this resource, * relative to the WPT folder. * @param {string} url the url of the resource being loaded. */ async readAsFetch(from, url) { const file = this.toRealFilePath(from, url); const data = await fsPromises.readFile(file); return { ok: true, arrayBuffer() { return data.buffer; }, json() { return JSON.parse(data.toString()); }, text() { return data.toString(); }, }; } } class StatusRule { constructor(key, value, pattern) { this.key = key; this.requires = value.requires || []; this.fail = value.fail; this.skip = value.skip; if (pattern) { this.pattern = this.transformPattern(pattern); } // TODO(joyeecheung): implement this this.scope = value.scope; this.comment = value.comment; } /** * Transform a filename pattern into a RegExp * @param {string} pattern * @returns {RegExp} */ transformPattern(pattern) { const result = path.normalize(pattern).replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&'); return new RegExp(result.replace('*', '.*')); } } class StatusRuleSet { constructor() { // We use two sets of rules to speed up matching this.exactMatch = {}; this.patternMatch = []; } /** * @param {object} rules */ addRules(rules) { for (const key of Object.keys(rules)) { if (key.includes('*')) { this.patternMatch.push(new StatusRule(key, rules[key], key)); } else { const normalizedPath = path.normalize(key); this.exactMatch[normalizedPath] = new StatusRule(key, rules[key]); } } } match(file) { const result = []; const exact = this.exactMatch[file]; if (exact) { result.push(exact); } for (const item of this.patternMatch) { if (item.pattern.test(file)) { result.push(item); } } return result; } } // A specification of WPT test class WPTTestSpec { #content; /** * @param {string} mod name of the WPT module, e.g. * 'html/webappapis/microtask-queuing' * @param {string} filename path of the test, relative to mod, e.g. * 'test.any.js' * @param {StatusRule[]} rules * @param {string} variant test file variant */ constructor(mod, filename, rules, variant = '') { this.module = mod; this.filename = filename; this.variant = variant; this.requires = new Set(); this.failedTests = []; this.flakyTests = []; this.skipReasons = []; for (const item of rules) { if (item.requires.length) { for (const req of item.requires) { this.requires.add(req); } } if (Array.isArray(item.fail?.expected)) { this.failedTests.push(...item.fail.expected); } if (Array.isArray(item.fail?.flaky)) { this.failedTests.push(...item.fail.flaky); this.flakyTests.push(...item.fail.flaky); } if (item.skip) { this.skipReasons.push(item.skip); } } this.failedTests = [...new Set(this.failedTests)]; this.flakyTests = [...new Set(this.flakyTests)]; this.skipReasons = [...new Set(this.skipReasons)]; } /** * @param {string} mod * @param {string} filename * @param {StatusRule[]} rules */ static from(mod, filename, rules) { const spec = new WPTTestSpec(mod, filename, rules); const meta = spec.getMeta(); return meta.variant?.map((variant) => new WPTTestSpec(mod, filename, rules, variant)) || [spec]; } getRelativePath() { return path.join(this.module, this.filename); } getAbsolutePath() { return fixtures.path('wpt', this.getRelativePath()); } /** * @returns {string} */ getContent() { this.#content ||= fs.readFileSync(this.getAbsolutePath(), 'utf8'); return this.#content; } /** * @returns {{ script?: string[]; variant?: string[]; [key: string]: string }} parsed META tags of a spec file */ getMeta() { const matches = this.getContent().match(/\/\/ META: .+/g); if (!matches) { return {}; } const result = {}; for (const match of matches) { const parts = match.match(/\/\/ META: ([^=]+?)=(.+)/); const key = parts[1]; const value = parts[2]; if (key === 'script' || key === 'variant') { if (result[key]) { result[key].push(value); } else { result[key] = [value]; } } else { result[key] = value; } } return result; } } const kIntlRequirement = { none: 0, small: 1, full: 2, // TODO(joyeecheung): we may need to deal with --with-intl=system-icu }; class BuildRequirement { constructor() { this.currentIntl = kIntlRequirement.none; if (process.config.variables.v8_enable_i18n_support === 0) { this.currentIntl = kIntlRequirement.none; return; } // i18n enabled if (process.config.variables.icu_small) { this.currentIntl = kIntlRequirement.small; } else { this.currentIntl = kIntlRequirement.full; } // Not using common.hasCrypto because of the global leak checks this.hasCrypto = Boolean(process.versions.openssl) && !process.env.NODE_SKIP_CRYPTO; } /** * @param {Set} requires * @returns {string|false} The config that the build is lacking, or false */ isLacking(requires) { const current = this.currentIntl; if (requires.has('full-icu') && current !== kIntlRequirement.full) { return 'full-icu'; } if (requires.has('small-icu') && current < kIntlRequirement.small) { return 'small-icu'; } if (requires.has('crypto') && !this.hasCrypto) { return 'crypto'; } return false; } } const buildRequirements = new BuildRequirement(); class StatusLoader { /** * @param {string} path relative path of the WPT subset */ constructor(path) { this.path = path; this.rules = new StatusRuleSet(); /** @type {WPTTestSpec[]} */ this.specs = []; } /** * Grep for all .*.js file recursively in a directory. * @param {string} dir */ grep(dir) { let result = []; const list = fs.readdirSync(dir); for (const file of list) { const filepath = path.join(dir, file); const stat = fs.statSync(filepath); if (stat.isDirectory()) { const list = this.grep(filepath); result = result.concat(list); } else { if (!(/\.\w+\.js$/.test(filepath))) { continue; } result.push(filepath); } } return result; } load() { const dir = path.join(__dirname, '..', 'wpt'); const statusFile = path.join(dir, 'status', `${this.path}.json`); const result = JSON.parse(fs.readFileSync(statusFile, 'utf8')); this.rules.addRules(result); const subDir = fixtures.path('wpt', this.path); const list = this.grep(subDir); for (const file of list) { const relativePath = path.relative(subDir, file); const match = this.rules.match(relativePath); this.specs.push(...WPTTestSpec.from(this.path, relativePath, match)); } } } const kPass = 'pass'; const kFail = 'fail'; const kSkip = 'skip'; const kTimeout = 'timeout'; const kIncomplete = 'incomplete'; const kUncaught = 'uncaught'; const NODE_UNCAUGHT = 100; const limit = (concurrency) => { let running = 0; const queue = []; const execute = async (fn) => { if (running < concurrency) { running++; try { await fn(); } finally { running--; if (queue.length > 0) { execute(queue.shift()); } } } else { queue.push(fn); } }; return execute; }; class WPTRunner { constructor(path, { concurrency = os.availableParallelism() - 1 || 1 } = {}) { this.path = path; this.resource = new ResourceLoader(path); this.concurrency = concurrency; this.flags = []; this.globalThisInitScripts = []; this.initScript = null; this.status = new StatusLoader(path); this.status.load(); this.specs = new Set(this.status.specs); this.results = {}; this.inProgress = new Set(); this.workers = new Map(); this.unexpectedFailures = []; if (process.env.WPT_REPORT != null) { this.report = new WPTReport(path); } } /** * Sets the Node.js flags passed to the worker. * @param {string[]} flags */ setFlags(flags) { this.flags = flags; } /** * Sets a script to be run in the worker before executing the tests. * @param {string} script */ setInitScript(script) { this.initScript = script; } /** * Set the scripts modifier for each script. * @param {(meta: { code: string, filename: string }) => void} modifier */ setScriptModifier(modifier) { this.scriptsModifier = modifier; } /** * @param {WPTTestSpec} spec */ fullInitScript(spec) { const url = new URL(`/${spec.getRelativePath().replace(/\.js$/, '.html')}${spec.variant}`, 'http://wpt'); const title = spec.getMeta().title; let { initScript } = this; initScript = `${initScript}\n\n//===\nglobalThis.location = new URL("${url.href}");`; if (title) { initScript = `${initScript}\n\n//===\nglobalThis.META_TITLE = "${title}";`; } if (this.globalThisInitScripts.length === null) { return initScript; } const globalThisInitScript = this.globalThisInitScripts.join('\n\n//===\n'); if (initScript === null) { return globalThisInitScript; } return `${globalThisInitScript}\n\n//===\n${initScript}`; } /** * Pretend the runner is run in `name`'s environment (globalThis). * @param {'Window'} name * @see {@link https://github.com/nodejs/node/blob/24673ace8ae196bd1c6d4676507d6e8c94cf0b90/test/fixtures/wpt/resources/idlharness.js#L654-L671} */ pretendGlobalThisAs(name) { switch (name) { case 'Window': { this.globalThisInitScripts.push('globalThis.Window = Object.getPrototypeOf(globalThis).constructor;'); this.loadLazyGlobals(); break; } // TODO(XadillaX): implement `ServiceWorkerGlobalScope`, // `DedicateWorkerGlobalScope`, etc. // // e.g. `ServiceWorkerGlobalScope` should implement dummy // `addEventListener` and so on. default: throw new Error(`Invalid globalThis type ${name}.`); } } loadLazyGlobals() { const lazyProperties = [ 'DOMException', 'Performance', 'PerformanceEntry', 'PerformanceMark', 'PerformanceMeasure', 'PerformanceObserver', 'PerformanceObserverEntryList', 'PerformanceResourceTiming', 'Blob', 'atob', 'btoa', 'MessageChannel', 'MessagePort', 'MessageEvent', 'EventTarget', 'Event', 'AbortController', 'AbortSignal', 'performance', 'TransformStream', 'TransformStreamDefaultController', 'WritableStream', 'WritableStreamDefaultController', 'WritableStreamDefaultWriter', 'ReadableStream', 'ReadableStreamDefaultReader', 'ReadableStreamBYOBReader', 'ReadableStreamBYOBRequest', 'ReadableByteStreamController', 'ReadableStreamDefaultController', 'ByteLengthQueuingStrategy', 'CountQueuingStrategy', 'TextEncoder', 'TextDecoder', 'TextEncoderStream', 'TextDecoderStream', 'CompressionStream', 'DecompressionStream', ]; if (Boolean(process.versions.openssl) && !process.env.NODE_SKIP_CRYPTO) { lazyProperties.push('crypto', 'Crypto', 'CryptoKey', 'SubtleCrypto'); } const script = lazyProperties.map((name) => `globalThis.${name};`).join('\n'); this.globalThisInitScripts.push(script); } // TODO(joyeecheung): work with the upstream to port more tests in .html // to .js. async runJsTests() { const queue = this.buildQueue(); const run = limit(this.concurrency); for (const spec of queue) { const content = spec.getContent(); const meta = spec.getMeta(content); 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) => { const obj = { filename: this.resource.toRealFilePath(relativePath, script), code: this.resource.read(relativePath, script), }; this.scriptsModifier?.(obj); return obj; }) ?? []; // The actual test const obj = { code: content, filename: absolutePath, }; this.scriptsModifier?.(obj); scriptsToRun.push(obj); run(async () => { const worker = new Worker(workerPath, { execArgv: this.flags, workerData: { testRelativePath: relativePath, wptRunner: __filename, wptPath: this.path, initScript: this.fullInitScript(spec), harness: { code: fs.readFileSync(harnessPath, 'utf8'), filename: harnessPath, }, scriptsToRun, needsGc: !!meta.script?.find((script) => script === '/common/gc.js'), }, }); this.inProgress.add(spec); this.workers.set(spec, worker); const reportResult = this.report?.getResult(spec); worker.on('message', (message) => { switch (message.type) { case 'result': return this.resultCallback(spec, message.result, reportResult); case 'completion': return this.completionCallback(spec, message.status, reportResult); default: throw new Error(`Unexpected message from worker: ${message.type}`); } }); worker.on('error', (err) => { if (!this.inProgress.has(spec)) { // The test is already finished. Ignore errors that occur after it. // This can happen normally, for example in timers tests. return; } // Generate a subtest failure for visibility. // No need to record this synthetic failure with wpt.fyi. this.fail( spec, { status: NODE_UNCAUGHT, name: 'evaluation in WPTRunner.runJsTests()', message: err.message, stack: inspect(err), }, kUncaught, ); // Mark the whole test as failed in wpt.fyi report. reportResult?.finish('ERROR'); this.inProgress.delete(spec); }); await events.once(worker, 'exit').catch(() => {}); }); } process.on('exit', () => { for (const spec of this.inProgress) { // No need to record this synthetic failure with wpt.fyi. this.fail(spec, { name: 'Incomplete' }, kIncomplete); // Mark the whole test as failed in wpt.fyi report. const reportResult = this.report?.getResult(spec); reportResult?.finish('ERROR'); } inspect.defaultOptions.depth = Infinity; // Sorts the rules to have consistent output console.log(''); console.log(JSON.stringify(Object.keys(this.results).sort().reduce( (obj, key) => { obj[key] = this.results[key]; return obj; }, {}, ), null, 2)); const failures = []; let expectedFailures = 0; let skipped = 0; for (const [key, item] of Object.entries(this.results)) { if (item.fail?.unexpected) { failures.push(key); } if (item.fail?.expected) { expectedFailures++; } if (item.skip) { skipped++; } } const unexpectedPasses = []; for (const specs of queue) { const key = specs.filename; // File has no expected failures if (!specs.failedTests.length) { continue; } // File was (maybe even conditionally) skipped if (this.results[key]?.skip) { continue; } // Full check: every expected to fail test is present if (specs.failedTests.some((expectedToFail) => { if (specs.flakyTests.includes(expectedToFail)) { return false; } return this.results[key]?.fail?.expected?.includes(expectedToFail) !== true; })) { unexpectedPasses.push(key); continue; } } this.report?.write(); const ran = queue.length; const total = ran + skipped; const passed = ran - expectedFailures - failures.length; console.log(''); console.log(`Ran ${ran}/${total} tests, ${skipped} skipped,`, `${passed} passed, ${expectedFailures} expected failures,`, `${failures.length} unexpected failures,`, `${unexpectedPasses.length} unexpected passes`); if (failures.length > 0) { const file = path.join('test', 'wpt', 'status', `${this.path}.json`); throw new Error( `Found ${failures.length} unexpected failures. ` + `Consider updating ${file} for these files:\n${failures.join('\n')}`); } if (unexpectedPasses.length > 0) { const file = path.join('test', 'wpt', 'status', `${this.path}.json`); throw new Error( `Found ${unexpectedPasses.length} unexpected passes. ` + `Consider updating ${file} for these files:\n${unexpectedPasses.join('\n')}`); } }); } // Map WPT test status to strings getTestStatus(status) { switch (status) { case 1: return kFail; case 2: return kTimeout; case 3: return kIncomplete; case NODE_UNCAUGHT: return kUncaught; default: return kPass; } } /** * Report the status of each specific test case (there could be multiple * in one test file). * @param {WPTTestSpec} spec * @param {Test} test The Test object returned by WPT harness * @param {ReportResult} reportResult The report result object */ resultCallback(spec, test, reportResult) { const status = this.getTestStatus(test.status); if (status !== kPass) { this.fail(spec, test, status, reportResult); } else { this.succeed(test, status, reportResult); } } /** * Report the status of each WPT test (one per file) * @param {WPTTestSpec} spec * @param {object} harnessStatus - The status object returned by WPT harness. * @param {ReportResult} reportResult The report result object */ completionCallback(spec, harnessStatus, reportResult) { const status = this.getTestStatus(harnessStatus.status); // Treat it like a test case failure if (status === kTimeout) { // No need to record this synthetic failure with wpt.fyi. this.fail(spec, { name: 'WPT testharness timeout' }, kTimeout); // Mark the whole test as TIMEOUT in wpt.fyi report. reportResult?.finish('TIMEOUT'); } else if (status !== kPass) { // No need to record this synthetic failure with wpt.fyi. this.fail(spec, { status: status, name: 'WPT test harness error', message: harnessStatus.message, stack: harnessStatus.stack, }, status); // Mark the whole test as ERROR in wpt.fyi report. reportResult?.finish('ERROR'); } else { reportResult?.finish(); } this.inProgress.delete(spec); // Always force termination of the worker. Some tests allocate resources // that would otherwise keep it alive. this.workers.get(spec).terminate(); } addTestResult(spec, item) { let result = this.results[spec.filename]; if (!result) { result = this.results[spec.filename] = {}; } if (item.status === kSkip) { // { filename: { skip: 'reason' } } result[kSkip] = item.reason; } else { // { filename: { fail: { expected: [ ... ], // unexpected: [ ... ] } }} if (!result[item.status]) { result[item.status] = {}; } const key = item.expected ? 'expected' : 'unexpected'; if (!result[item.status][key]) { result[item.status][key] = []; } const hasName = result[item.status][key].includes(item.name); if (!hasName) { result[item.status][key].push(item.name); } } } succeed(test, status, reportResult) { console.log(`[${status.toUpperCase()}] ${test.name}`); reportResult?.addSubtest(test.name, 'PASS'); } fail(spec, test, status, reportResult) { const expected = spec.failedTests.includes(test.name); if (expected) { console.log(`[EXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`); } else { console.log(`[UNEXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`); } if (status === kFail || status === kUncaught) { console.log(test.message); console.log(test.stack); } const command = `${process.execPath} ${process.execArgv}` + ` ${require.main.filename} '${spec.filename}${spec.variant}'`; console.log(`Command: ${command}\n`); reportResult?.addSubtest(test.name, 'FAIL', test.message); this.addTestResult(spec, { name: test.name, expected, status: kFail, reason: test.message || status, }); } skip(spec, reasons) { const joinedReasons = reasons.join('; '); console.log(`[SKIPPED] ${spec.filename}${spec.variant}: ${joinedReasons}`); this.addTestResult(spec, { status: kSkip, reason: joinedReasons, }); } buildQueue() { const queue = []; let argFilename; let argVariant; if (process.argv[2]) { ([argFilename, argVariant = ''] = process.argv[2].split('?')); } for (const spec of this.specs) { if (argFilename) { if (spec.filename === argFilename && (!argVariant || spec.variant.substring(1) === argVariant)) { queue.push(spec); } continue; } if (spec.skipReasons.length > 0) { this.skip(spec, spec.skipReasons); continue; } const lackingSupport = buildRequirements.isLacking(spec.requires); if (lackingSupport) { this.skip(spec, [ `requires ${lackingSupport}` ]); continue; } queue.push(spec); } // If the tests are run as `node test/wpt/test-something.js subset.any.js`, // only `subset.any.js` (all variants) 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. if (argFilename && queue.length === 0) { throw new Error(`${process.argv[2]} not found!`); } return queue; } } module.exports = { harness: harnessMock, ResourceLoader, WPTRunner, };