UNPKG

@babel/helper-transform-fixture-test-runner

Version:

Transform test runner for @babel/helper-fixtures module

744 lines (738 loc) 25.2 kB
import * as babel from '@babel/core'; import { buildExternalHelpers } from '@babel/core'; import getFixtures, { readFile, resolveOptionPluginOrPreset } from '@babel/helper-fixtures'; import { codeFrameColumns } from '@babel/code-frame'; import { eachMapping, TraceMap } from '@jridgewell/trace-mapping'; import assert from 'node:assert'; import fs, { realpathSync, readFileSync } from 'node:fs'; import path from 'node:path'; import vm from 'node:vm'; import { LRUCache } from 'lru-cache'; import { fileURLToPath } from 'node:url'; import { diff } from 'jest-diff'; import { spawn } from 'node:child_process'; import os from 'node:os'; import * as resolve from 'resolve'; import { createRequire } from 'node:module'; import checkDuplicateNodes from '@babel/helper-check-duplicate-nodes'; import { createHash } from 'node:crypto'; function assertNoOwnProperties(obj) { expect(Object.getOwnPropertyNames(obj)).toHaveLength(0); } function multiline(arr) { return arr.join("\n"); } const helpers = /*#__PURE__*/Object.defineProperty({ __proto__: null, assertNoOwnProperties, multiline }, Symbol.toStringTag, { value: 'Module' }); const CONTEXT_SIZE = 4; const LOC_SIZE = 10; const CONTENT_SIZE = 15; function simpleCodeFramePoint(lines, line, col) { const start = Math.max(col - CONTEXT_SIZE, 0); const end = Math.min(col + 1 + CONTEXT_SIZE, lines[line - 1].length); const code = lines[line - 1].slice(start, end); const loc = `(${line}:${col}) `.padStart(LOC_SIZE, " "); return loc + code + "\n" + " ".repeat(col - start + loc.length) + "^"; } function joinMultiline(left, right, leftLen) { const leftLines = left.split("\n"); const rightLines = right.split("\n"); leftLen ??= leftLines.reduce((len, line) => Math.max(len, line.length), 0); const linesCount = Math.max(leftLines.length, rightLines.length); let res = ""; for (let i = 0; i < linesCount; i++) { if (res !== "") res += "\n"; if (i < leftLines.length) res += leftLines[i].padEnd(leftLen, " ");else res += " ".repeat(leftLen); if (i < rightLines.length) res += rightLines[i]; } return res; } function visualize(output, map) { const sourcesLines = new Map(map.sources.map((source, index) => [source, map.sourcesContent[index].split("\n")])); const outputLines = output.split("\n"); const ranges = []; let prev = null; eachMapping(new TraceMap(map), mapping => { if (prev === null) { prev = mapping; return; } const original = { from: { line: prev.originalLine, column: prev.originalColumn }, to: { line: prev.originalLine, column: prev.originalColumn + 1 } }; const generated = { from: { line: prev.generatedLine, column: prev.generatedColumn }, to: { line: prev.generatedLine, column: prev.generatedColumn + 1 } }; if (original.from.line !== original.to.line) { original.to.line = original.from.line; original.to.column = Infinity; } else if (original.to.column < original.from.column) { original.to.column = original.from.column; } if (generated.from.line !== generated.to.line) { generated.to.line = generated.from.line; generated.to.column = Infinity; } else if (generated.to.column < generated.from.column) { generated.to.column = generated.from.column; } ranges.push({ original, generated, source: prev.source }); prev = mapping; }); if (prev.originalLine) { ranges.push({ original: { from: { line: prev.originalLine, column: prev.originalColumn }, to: { line: prev.originalLine, column: prev.originalColumn + 1 } }, generated: { from: { line: prev.generatedLine, column: prev.generatedColumn }, to: { line: prev.generatedLine, column: prev.generatedColumn + 1 } }, source: prev.source }); } const res = ranges.map(({ original, generated, source }) => { const input = simpleCodeFramePoint(sourcesLines.get(source), original.from.line, original.from.column); const output = simpleCodeFramePoint(outputLines, generated.from.line, generated.from.column); return joinMultiline(joinMultiline(input, " <-- ", LOC_SIZE + CONTEXT_SIZE * 2 + CONTENT_SIZE), output); }); return res.join("\n\n"); } const require$1 = createRequire(import.meta.url); const dirname = path.dirname(fileURLToPath(import.meta.url)); const EXTERNAL_HELPERS_VERSION = "7.100.0"; const cachedScripts = new LRUCache({ max: 10 }); const contextModuleCache = new WeakMap(); function transformWithoutConfigFile(code, opts) { return babel.transformSync(code, { browserslistConfigFile: false, configFile: false, babelrc: false, caller: { name: "babel-helper-transform-fixture-test-runner/sync", supportsStaticESM: false, supportsDynamicImport: false, supportsExportNamespaceFrom: false }, ...opts }); } function transformAsyncWithoutConfigFile(code, opts) { return babel.transformAsync(code, { browserslistConfigFile: false, configFile: false, babelrc: false, caller: { name: "babel-helper-transform-fixture-test-runner/async", supportsStaticESM: false, supportsDynamicImport: false, supportsExportNamespaceFrom: false }, ...opts }); } function createTestContext() { const context = vm.createContext({ ...helpers, process: process, transform: transformWithoutConfigFile, transformAsync: transformAsyncWithoutConfigFile, setTimeout: setTimeout, setImmediate: setImmediate, expect }); context.global = context; const moduleCache = Object.create(null); contextModuleCache.set(context, moduleCache); runCacheableScriptInTestContext(path.join(path.dirname(fileURLToPath(import.meta.url)), "babel-helpers-in-memory.js"), buildExternalHelpers, context, moduleCache); return context; } function runCacheableScriptInTestContext(filename, srcFn, context, moduleCache) { let cached = cachedScripts.get(filename); if (!cached) { const code = `(function (exports, require, module, __filename, __dirname) {\n${srcFn()}\n});`; cached = { code, cachedData: undefined }; cachedScripts.set(filename, cached); } const script = new vm.Script(cached.code, { filename, lineOffset: -1, cachedData: cached.cachedData }); cached.cachedData = script.createCachedData(); const module = { id: filename, exports: {} }; moduleCache[filename] = module; const req = id => runModuleInTestContext(id, filename, context, moduleCache); const dirname = path.dirname(filename); script.runInContext(context).call(module.exports, module.exports, req, module, filename, dirname); return module; } function runModuleInTestContext(id, relativeFilename, context, moduleCache) { const filename = resolve.sync(id, { basedir: path.dirname(relativeFilename) }); if (filename === id) return require$1(id); if (moduleCache[filename]) return moduleCache[filename].exports; return runCacheableScriptInTestContext(filename, () => fs.readFileSync(filename, "utf8"), context, moduleCache).exports; } let sharedTestContext; function runCodeInTestContext(code, opts, context = sharedTestContext ??= createTestContext()) { const filename = opts.filename instanceof URL ? fileURLToPath(opts.filename) : opts.filename; const dirname = path.dirname(filename); const moduleCache = contextModuleCache.get(context); const req = id => runModuleInTestContext(id, filename, context, moduleCache); const module = { id: filename, exports: {} }; const oldCwd = process.cwd(); try { if (filename) process.chdir(path.dirname(filename)); const src = `((function(exports, require, module, __filename, __dirname, opts) {\n${code}\n})).apply(global, global.__callArgs);`; context.__callArgs = [module.exports, req, module, filename, dirname, opts]; return vm.runInContext(src, context, { filename, displayErrors: true, lineOffset: -1, timeout: opts.timeout ?? 10000 }); } finally { context.__callArgs = undefined; process.chdir(oldCwd); } } async function maybeMockConsole(validateLogs, run) { const actualLogs = { stdout: "", stderr: "" }; if (!validateLogs) return { result: await run(), actualLogs }; const spy1 = jest.spyOn(console, "log").mockImplementation(msg => { actualLogs.stdout += `${msg}\n`; }); const spy2 = jest.spyOn(console, "warn").mockImplementation(msg => { actualLogs.stderr += `${msg}\n`; }); try { return { result: await run(), actualLogs }; } finally { spy1.mockRestore(); spy2.mockRestore(); } } async function run(task) { const { actual, expect: expected, exec, options: opts, doNotSetSourceType, optionsDir, validateLogs, ignoreOutput, stdout, stderr } = task; function getOpts(self) { const newOpts = { ast: true, cwd: path.dirname(self.loc), filename: self.loc, filenameRelative: self.filename, sourceFileName: self.filename, ...(doNotSetSourceType ? {} : { sourceType: "script" }), babelrc: false, configFile: false, inputSourceMap: task.inputSourceMap || undefined, ...opts }; return resolveOptionPluginOrPreset(newOpts, optionsDir); } let execCode = exec.code; let result; let resultExec; let execErr; if (execCode) { const context = createTestContext(); const execOpts = getOpts(exec); result = (await maybeMockConsole(validateLogs, async () => babel.transformAsync(execCode, execOpts))).result; checkDuplicateNodes(result.ast); execCode = result.code; try { resultExec = runCodeInTestContext(execCode, execOpts, context); } catch (err) { if (typeof err === "object" && err.message) { err.message = `${exec.loc}: ${err.message}\n` + codeFrameColumns(execCode, {}); } execErr = err; } } const inputCode = actual.code; const expectedCode = expected.code; if (!execCode || inputCode) { const res = await maybeMockConsole(validateLogs, () => babel.transformAsync(inputCode, getOpts(actual))); result = res.result; const outputCode = normalizeOutput(result.code, { normalizePathSeparator: true }); checkDuplicateNodes(result.ast); if (!ignoreOutput) { if (!expectedCode && outputCode && !opts.throws && fs.statSync(path.dirname(expected.loc)).isDirectory() && !process.env.CI) { const expectedFile = expected.loc.replace(/\.m?js$/, result.sourceType === "module" ? ".mjs" : ".js"); console.log(`New test file created: ${expectedFile}`); fs.writeFileSync(expectedFile, `${outputCode}\n`); if (expected.loc !== expectedFile) { try { fs.unlinkSync(expected.loc); } catch (_) {} } } else { validateFile(outputCode, expected.loc, expectedCode); if (inputCode) { expect(expected.loc).toMatch(result.sourceType === "module" ? /\.mjs$/ : /\.js$/); } } } if (validateLogs) { const normalizationOpts = { normalizePathSeparator: true, normalizePresetEnvDebug: task.taskDir.includes("babel-preset-env") }; validateFile(normalizeOutput(res.actualLogs.stdout, normalizationOpts), stdout.loc, stdout.code); validateFile(normalizeOutput(res.actualLogs.stderr, normalizationOpts), stderr.loc, stderr.code); } } if (execErr) { throw execErr; } if (task.validateSourceMapVisual === true) { const visual = visualize(result.code, result.map); try { expect(visual).toEqual(task.sourceMapVisual.code); } catch (e) { if (!process.env.OVERWRITE && task.sourceMapVisual.code) throw e; console.log(`Updated test file: ${task.sourceMapVisual.loc}`); fs.writeFileSync(task.sourceMapVisual.loc ?? task.taskDir + "/source-map-visual.txt", visual + "\n"); } } if (opts.sourceMaps === true) { try { expect(result.map).toEqual(task.sourceMap); } catch (e) { if (!process.env.OVERWRITE && task.sourceMap) throw e; task.sourceMapFile.loc ??= task.taskDir + "/source-map.json"; console.log(`Updated test file: ${task.sourceMapFile.loc}`); fs.writeFileSync(task.sourceMapFile.loc, JSON.stringify(result.map, null, 2)); } } if (execCode && resultExec) { return resultExec; } } function validateFile(actualCode, expectedLoc, expectedCode) { if (actualCode !== expectedCode) { if (process.env.OVERWRITE) { console.log(`Updated test file: ${expectedLoc}`); fs.writeFileSync(expectedLoc, `${actualCode}\n`); return; } throw new Error(`Expected ${expectedLoc} to match transform output.\n` + `To autogenerate a passing version of this file, delete ` + ` the file and re-run the tests.\n\n` + `Diff:\n\n${diff(expectedCode, actualCode, { expand: false })}`); } } function normalizeOutput(code, { normalizePathSeparator = false } = {}) { const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../"); const symbol = "<CWD>"; let result = code.trim().replaceAll(dir, symbol); if (process.platform === "win32") { result = result.replaceAll(dir.replaceAll("\\", "/"), symbol).replaceAll(dir.replaceAll("\\", "\\\\"), symbol); if (normalizePathSeparator) { result = result.replaceAll(/<CWD>[\w\\/.-]+/g, path => path.replaceAll(/\\\\?/g, "/")); } } return result; } function index (fixturesLoc, name, suiteOpts = {}, taskOpts = {}, dynamicOpts) { const suites = getFixtures(fixturesLoc); for (const testSuite of suites) { if (suiteOpts.ignoreSuites?.includes(testSuite.title)) continue; describe(name + "/" + testSuite.title, function () { for (const task of testSuite.tests) { if (suiteOpts.ignoreTasks?.includes(task.title) || suiteOpts.ignoreTasks?.includes(testSuite.title + "/" + task.title)) { continue; } const testFn = task.disabled ? it.skip : it; const testTitle = typeof task.disabled === "string" ? `(SKIP: ${task.disabled}) ${task.title}` : task.title; testFn(testTitle, async function () { const runTask = () => run(task); if ("sourceMap" in task.options) { throw new Error("`sourceMap` option is deprecated. Use `sourceMaps` instead."); } if ("sourceMaps" in task.options === false) { task.options.sourceMaps = !!task.sourceMap; } Object.assign(task.options, taskOpts); if (dynamicOpts) dynamicOpts(task.options, task); if (task.externalHelpers) { (task.options.plugins ??= []).push(["external-helpers", { helperVersion: EXTERNAL_HELPERS_VERSION }]); } const throwMsg = task.options.throws; if (throwMsg) { delete task.options.throws; await assert.rejects(runTask, function (err) { assert.ok(throwMsg === true || err.message.includes(throwMsg), ` Expected Error: ${throwMsg} Actual Error: ${err.message}`); return true; }); } else { return runTask(); } }); } }); } } const tmpDir = realpathSync.native(os.tmpdir()); const readDir = function (loc, pathFilter) { const files = {}; if (fs.existsSync(loc)) { fs.readdirSync(loc, { withFileTypes: true, recursive: true }).filter(dirent => dirent.isFile() && pathFilter(dirent.name)).forEach(dirent => { const fullpath = path.join(dirent.parentPath, dirent.name); files[path.relative(loc, fullpath)] = readFile(fullpath); }); } return files; }; const outputFileSync = function (filePath, data) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, data); }; function deleteDir(path) { fs.rmSync(path, { force: true, recursive: true }); } const pathFilter = function (x) { return path.basename(x) !== ".DS_Store"; }; const assertTest = function (stdout, stderr, ipcMessage, opts, tmpDir) { const expectStderr = opts.stderr.trim(); stderr = stderr.trim(); try { if (opts.stderr) { if (opts.stderrContains) { expect(stderr).toContain(expectStderr); } else { expect(stderr).toBe(expectStderr); } } else if (stderr) { throw new Error("stderr:\n" + stderr); } } catch (e) { if (!process.env.OVERWRITE) throw e; console.log(`Updated test file: ${opts.stderrPath}`); outputFileSync(opts.stderrPath, stderr + "\n"); } const expectStdout = opts.stdout.trim(); stdout = stdout.trim(); stdout = stdout.replace(/\\/g, "/"); try { if (opts.stdout) { if (opts.stdoutContains) { expect(stdout).toContain(expectStdout); } else { expect(stdout).toBe(expectStdout); } } else if (stdout) { throw new Error("stdout:\n" + stdout); } } catch (e) { if (!process.env.OVERWRITE) throw e; console.log(`Updated test file: ${opts.stdoutPath}`); outputFileSync(opts.stdoutPath, stdout + "\n"); } if (opts.ipc) { expect(ipcMessage).toEqual(opts.ipcMessage); } if (opts.outFiles) { const actualFiles = readDir(tmpDir, pathFilter); Object.keys(actualFiles).forEach(function (filename) { try { if (filename !== ".babelrc" && filename !== ".babelignore" && !Object.hasOwn(opts.inFiles, filename)) { const expected = opts.outFiles[filename]; const actual = actualFiles[filename]; expect(actual).toBe(expected || ""); } } catch (e) { if (!process.env.OVERWRITE) { e.message += "\n at " + filename; throw e; } const expectedLoc = path.join(opts.testLoc, "out-files", filename); console.log(`Updated test file: ${expectedLoc}`); outputFileSync(expectedLoc, actualFiles[filename]); } }); Object.keys(opts.outFiles).forEach(function (filename) { expect(actualFiles).toHaveProperty([filename]); }); } }; function buildParallelProcessTests(name, tests) { return function (curr, total) { const sliceLength = Math.ceil(tests.length / total); const sliceStart = curr * sliceLength; const sliceEnd = sliceStart + sliceLength; const testsSlice = tests.slice(sliceStart, sliceEnd); describe(`${name} [${curr}/${total}]`, function () { it("dummy", () => {}); for (const test of testsSlice) { (test.skip ? it.skip : it)(test.suiteName + " " + test.testName, test.fn); } }); }; } function buildProcessTests(dir, beforeHook, afterHook) { if (dir instanceof URL) dir = fileURLToPath(dir); const tests = []; fs.readdirSync(dir).forEach(function (suiteName) { if (suiteName.startsWith(".") || suiteName === "package.json") return; const suiteLoc = path.join(dir, suiteName); fs.readdirSync(suiteLoc).forEach(function (testName) { if (testName.startsWith(".")) return; const testLoc = path.join(suiteLoc, testName); let opts = { args: [], stdout: "", stderr: "", stdin: "", stdoutPath: "", stderrPath: "", testLoc: "", outFiles: {}, inFiles: {} }; const optionsLoc = path.join(testLoc, "options.json"); if (fs.existsSync(optionsLoc)) { const taskOpts = JSON.parse(readFileSync(optionsLoc, "utf8")); if (taskOpts.os) { let os = taskOpts.os; if (!Array.isArray(os) && typeof os !== "string") { throw new Error(`'os' should be either string or string array: ${taskOpts.os}`); } if (typeof os === "string") { os = [os]; } if (!os.includes(process.platform)) { return; } delete taskOpts.os; } opts = { args: [], ...taskOpts }; } const executorLoc = path.join(testLoc, "executor.js"); if (fs.existsSync(executorLoc)) { opts.executor = executorLoc; } opts.stderrPath = path.join(testLoc, "stderr.txt"); opts.stdoutPath = path.join(testLoc, "stdout.txt"); for (const key of ["stdout", "stdin", "stderr"]) { const loc = path.join(testLoc, key + ".txt"); if (fs.existsSync(loc)) { opts[key] = readFile(loc); } else { opts[key] = opts[key] || ""; } } opts.testLoc = testLoc; opts.outFiles = readDir(path.join(testLoc, "out-files"), pathFilter); opts.inFiles = readDir(path.join(testLoc, "in-files"), pathFilter); const babelrcLoc = path.join(testLoc, ".babelrc"); const babelIgnoreLoc = path.join(testLoc, ".babelignore"); if (fs.existsSync(babelrcLoc)) { opts.inFiles[".babelrc"] = readFile(babelrcLoc); } else if (!opts.noBabelrc) { opts.inFiles[".babelrc"] = "{}"; } if (fs.existsSync(babelIgnoreLoc)) { opts.inFiles[".babelignore"] = readFile(babelIgnoreLoc); } const skip = opts.minNodeVersion && parseInt(process.versions.node, 10) < opts.minNodeVersion || opts.BABEL_8_BREAKING === false; const test = { suiteName, testName, skip, opts, fn: function (callback) { const tmpLoc = path.join(tmpDir, "babel-process-test", createHash("sha1").update(testLoc).digest("hex")); deleteDir(tmpLoc); fs.mkdirSync(tmpLoc, { recursive: true }); const { inFiles } = opts; for (const filename of Object.keys(inFiles)) { outputFileSync(path.join(tmpLoc, filename), inFiles[filename]); } try { beforeHook(test, tmpLoc); if (test.binLoc === undefined) { throw new Error("test.binLoc is undefined"); } let args = opts.executor ? ["--require", path.join(dirname, "./exit-loader.cjs"), test.binLoc] : [test.binLoc]; args = args.concat(opts.args); const env = { ...process.env, FORCE_COLOR: "false", ...(parseInt(process.versions.node) >= 22 && { NODE_OPTIONS: "--disable-warning=ExperimentalWarning" }), ...opts.env }; const child = spawn(process.execPath, args, { env, cwd: tmpLoc, stdio: opts.executor || opts.ipc ? ["pipe", "pipe", "pipe", "ipc"] : "pipe" }); let stderr = ""; let stdout = ""; let ipcMessage; child.on("close", function () { let err; try { const result = afterHook ? afterHook(test, tmpLoc, stdout, stderr) : { stdout, stderr }; assertTest(result.stdout, result.stderr, ipcMessage, opts, tmpLoc); } catch (e) { err = e; } finally { try { deleteDir(tmpLoc); } catch (error) { console.error(error); } } if (err) { err.message = args.map(arg => `"${arg}"`).join(" ") + ": " + err.message; } callback(err); }); if (opts.ipc) { child.on("message", function (message) { ipcMessage = message; }); } if (opts.stdin) { child.stdin.write(opts.stdin); child.stdin.end(); } const captureOutput = proc => { proc.stderr.on("data", function (chunk) { stderr += chunk; }); proc.stdout.on("data", function (chunk) { stdout += chunk; }); }; if (opts.executor) { const executor = spawn(process.execPath, [opts.executor], { cwd: tmpLoc }); child.stdout.pipe(executor.stdin); child.stderr.pipe(executor.stdin); executor.on("close", function () { child.send("exit"); }); captureOutput(executor); } else { captureOutput(child); } } catch (e) { deleteDir(tmpLoc); throw e; } } }; tests.push(test); }); }); tests.sort(function (testA, testB) { const nameA = testA.suiteName + "/" + testA.testName; const nameB = testB.suiteName + "/" + testB.testName; return nameA.localeCompare(nameB); }); return tests; } export { buildParallelProcessTests, buildProcessTests, createTestContext, index as default, runCodeInTestContext }; //# sourceMappingURL=index.js.map