#! /usr/bin/env node import { constants as fs, readFileSync, mkdtempSync, existsSync, statSync, mkdirSync, accessSync, appendFileSync, readdirSync, rmSync, } from "node:fs"; import { spawn, spawnSync } from "node:child_process"; import { tmpdir, hostname } from "node:os"; import { join, basename, dirname, relative } from "node:path"; import { normalize as normalizeWindows } from "node:path/win32"; const spawnTimeout = 30_000; const softTestTimeout = 60_000; const hardTestTimeout = 3 * softTestTimeout; const isMacOS = process.platform === "darwin"; const isWindows = process.platform === "win32"; const isGitHubAction = !!process.env["GITHUB_ACTIONS"]; const isBuildKite = !!process.env["BUILDKITE"]; const isBuildKiteTestSuite = !!process.env["BUILDKITE_ANALYTICS_TOKEN"]; const isCI = !!process.env["CI"] || isGitHubAction || isBuildKite; const isInteractive = !isCI && process.argv.includes("-i") && process.stdout.isTTY; const shardId = parseInt(process.env["BUILDKITE_PARALLEL_JOB"]) || 0; const maxShards = parseInt(process.env["BUILDKITE_PARALLEL_JOB_COUNT"]) || 1; const baseUrl = process.env["GITHUB_SERVER_URL"] || "https://github.com"; const repository = process.env["GITHUB_REPOSITORY"] || "oven-sh/bun"; const pullRequest = /^pull\/(\d+)$/.exec(process.env["GITHUB_REF"])?.[1]; const gitSha = getGitSha(); const gitRef = getGitRef(); const cwd = dirname(import.meta.dirname); const testsPath = join(cwd, "test"); const tmpPath = getTmpdir(); function printInfo() { console.log("Timestamp:", new Date()); console.log("OS:", getOsPrettyText(), getOsEmoji()); console.log("Arch:", getArchText(), getArchEmoji()); console.log("Hostname:", getHostname()); if (isCI) { console.log("CI:", getCI()); console.log("Shard:", shardId, "/", maxShards); console.log("Build URL:", getBuildUrl()); console.log("Environment:", process.env); } console.log("Cwd:", cwd); console.log("Tmpdir:", tmpPath); console.log("Commit:", gitSha); console.log("Ref:", gitRef); if (pullRequest) { console.log("Pull Request:", pullRequest); } } /** * @param {string} target * @returns {Promise} */ async function runTests(target) { let execPath; if (isBuildKite) { execPath = await getExecPathFromBuildKite(target); } else { execPath = getExecPath(target); } console.log("Bun:", execPath); const revision = getRevision(execPath); console.log("Revision:", revision); const tests = getTests(testsPath); const firstTest = shardId * Math.ceil(tests.length / maxShards); const lastTest = Math.min(firstTest + Math.ceil(tests.length / maxShards), tests.length); console.log("Running tests:", firstTest, "...", lastTest, "/", tests.length); const results = []; /** * @param {string} title * @param {function} fn */ const runTest = async (title, fn) => { const result = await runTask(title, fn); if (isBuildKite) { const { testPath, stdout } = result; const logsPath = join(cwd, "logs", `${testPath}.log`); mkdirSync(dirname(logsPath), { recursive: true }); appendFileSync(logsPath, stripAnsi(stdout)); const markdown = formatTestToMarkdown(result); if (markdown) { const label = getBuildLabel(); reportAnnotationToBuildKite(label, markdown); } } else if (isGitHubAction) { const summaryPath = process.env["GITHUB_STEP_SUMMARY"]; if (summaryPath) { const markdown = formatTestToMarkdown(result); if (markdown) { appendFileSync(summaryPath, markdown); } } } }; for (const path of [cwd, testsPath]) { const title = relative(cwd, join(path, "package.json")).replace(/\\/g, "/"); await runTest(title, async () => spawnBunInstall(execPath, { cwd: path })); } if (results.some(({ ok }) => !ok)) { // If install failed, don't run the tests. return 1; } for (const testPath of tests.slice(firstTest, lastTest)) { const title = relative(cwd, join(testsPath, testPath)).replace(/\\/g, "/"); await runTest(title, async () => spawnBunTest(execPath, join("test", testPath))); } return results.some(({ ok }) => !ok) ? 1 : 0; } /** * @typedef {object} SpawnOptions * @property {string} command * @property {string[]} [args] * @property {string} [cwd] * @property {number} [timeout] * @property {object} [env] * @property {function} [stdout] * @property {function} [stderr] */ /** * @typedef {object} SpawnResult * @property {boolean} ok * @property {string} [error] * @property {Error} [spawnError] * @property {number} [exitCode] * @property {number} [signalCode] * @property {number} timestamp * @property {number} duration * @property {string} stdout */ /** * @param {SpawnOptions} request * @returns {Promise} */ async function spawnSafe({ command, args, cwd, env, timeout = spawnTimeout, stdout = process.stdout.write.bind(process.stdout), stderr = process.stderr.write.bind(process.stderr), }) { let exitCode; let signalCode; let spawnError; let timestamp; let duration; let subprocess; let timer; let buffer = ""; let doneCalls = 0; const beforeDone = resolve => { // TODO: wait for stderr as well, spawn.test currently causes it to hang if (doneCalls++ === 1) { done(resolve); } }; const done = resolve => { if (timer) { clearTimeout(timer); } subprocess.stderr.unref(); subprocess.stdout.unref(); subprocess.unref(); if (!signalCode && exitCode === undefined) { subprocess.stdout.destroy(); subprocess.stderr.destroy(); subprocess.kill(9); } resolve(); }; await new Promise(resolve => { try { subprocess = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], timeout, cwd, env, }); subprocess.on("spawn", () => { timestamp = Date.now(); timer = setTimeout(() => done(resolve), timeout); }); subprocess.on("error", error => { spawnError = error; done(resolve); }); subprocess.on("exit", (code, signal) => { duration = Date.now() - timestamp; exitCode = code; signalCode = signal; if (signalCode || exitCode !== 0) { beforeDone(resolve); } else { done(resolve); } }); subprocess.stdout.on("end", () => { beforeDone(resolve); }); subprocess.stdout.on("data", chunk => { const text = chunk.toString("utf-8"); stdout?.(text); buffer += text; }); subprocess.stderr.on("data", chunk => { const text = chunk.toString("utf-8"); stderr?.(text); buffer += text; }); } catch (error) { spawnError = error; resolve(); } }); let error; if (spawnError) { const { message } = spawnError; if (/timed? ?out/.test(message)) { error = "timeout"; } else { error = `error: ${message}`; } } else if (signalCode) { if (signalCode === "SIGTERM" && duration >= timeout) { error = "timeout"; } else { error = signalCode; } } else if ((error = /thread \d+ panic: (.*)/.test(buffer))) { error = `panic: ${error[1]}`; } else if (exitCode === 1) { const match = buffer.match(/\x1b\[31m\s(\d+) fail/); if (match) { error = `${match[1]} failing`; } else { error = "code 1"; } } else if (exitCode === undefined) { error = "timeout"; } else if (exitCode !== 0) { if (isWindows) { const winCode = getWindowsExitCode(exitCode); if (winCode) { exitCode = winCode; } } error = `code ${exitCode}`; } return { ok: exitCode === 0 && !signalCode && !spawnError, error, exitCode, signalCode, spawnError, stdout: buffer, timestamp: timestamp || Date.now(), duration: duration || 0, }; } /** * @param {string} execPath * @param {SpawnOptions} options * @returns {Promise} */ async function spawnBun(execPath, { args, cwd, timeout, env, stdout, stderr }) { const testPath = mkdtempSync(join(tmpPath, "bun-")); const tmpdirPath = mkdtempSync(join(testPath, "tmp-")); const homePath = mkdtempSync(join(testPath, "home-")); const cachePath = mkdtempSync(join(testPath, "cache-")); const bunEnv = { [isWindows ? "Path" : "PATH"]: addPath(dirname(execPath), process.env.PATH), USER: process.env.USER, HOME: homePath, [isWindows ? "TEMP" : "TMPDIR"]: tmpdirPath, FORCE_COLOR: "1", BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: "1", BUN_DEBUG_QUIET_LOGS: "1", BUN_GARBAGE_COLLECTOR_LEVEL: "1", BUN_ENABLE_CRASH_REPORTING: "1", BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0", BUN_INSTALL_CACHE_DIR: cachePath, SHELLOPTS: isWindows ? "igncr" : undefined, // ignore "\r" on Windows }; if (env) { Object.assign(bunEnv, env); } try { return await spawnSafe({ command: execPath, args, cwd, timeout, env: bunEnv, stdout, stderr, }); } finally { try { rmSync(testPath, { recursive: true, force: true }); } catch (error) { console.error(error); } } } /** * @typedef {object} TestResult * @property {string} testPath * @property {boolean} ok * @property {string} status * @property {string} [error] * @property {TestEntry[]} tests * @property {string} stdout * @property {string} stderr */ /** * @typedef {object} TestEntry * @property {string} [url] * @property {string} file * @property {string} test * @property {string} status * @property {TestError} [error] * @property {number} [duration] */ /** * @typedef {object} TestError * @property {string} [url] * @property {string} file * @property {number} line * @property {number} col * @property {string} name * @property {string} stack */ /** * * @param {string} execPath * @param {string} testPath * @returns {Promise} */ async function spawnBunTest(execPath, testPath) { const timeout = isSequentialTest(testPath) ? hardTestTimeout : softTestTimeout; const { ok, error, stdout } = await spawnBun(execPath, { args: ["test", testPath], cwd: cwd, timeout: timeout, env: { GITHUB_ACTIONS: "true", // always true so annotations are parsed }, stdout: chunk => pipeTestStdout(process.stdout, chunk), stderr: chunk => pipeTestStdout(process.stderr, chunk), }); const { tests, errors, stdout: testStdout } = parseTestStdout(stdout, testPath); return { testPath, ok, status: ok ? "pass" : "fail", error, errors, tests, stdout: testStdout, }; } /** * @param {NodeJS.WritableStream} io * @param {string} chunk */ function pipeTestStdout(io, chunk) { if (isGitHubAction) { io.write(chunk); } else { io.write(chunk.replace(/\:\:.*(?:\r\n|\r|\n)/gim, "")); } } /** * @typedef {object} TestOutput * @property {string} stdout * @property {TestResult[]} tests * @property {TestError[]} errors */ /** * @param {string} stdout * @param {string} [testPath] * @returns {TestOutput} */ function parseTestStdout(stdout, testPath) { const tests = []; const errors = []; const lines = []; let skipCount = 0; let testErrors = []; let done; for (const chunk of stdout.split("\n")) { const string = stripAnsi(chunk); if (!string.startsWith("::")) { if (skipCount === 0) { lines.push(chunk); } if (string.startsWith("✓")) { skipCount++; } else { if (skipCount > 0) { const omitLine = `${getAnsi("gray")}... omitted ${skipCount} tests ...${getAnsi("reset")}`; lines.push(omitLine, chunk); } skipCount = 0; } } // Once the summary is printed, exit early so tests aren't double counted. // This needs to be changed if multiple files are run in a single test run. if (done || string.startsWith("::endgroup")) { done ||= true; continue; } if (string.startsWith("::error")) { const eol = string.indexOf("::", 8); const message = unescapeGitHubAction(string.substring(eol + 2)); const { file, line, col, title } = Object.fromEntries( string .substring(8, eol) .split(",") .map(entry => entry.split("=")), ); const errorPath = file || testPath; const error = { url: getFileUrl(errorPath, line), file: errorPath, line, col, name: title, stack: `${title}\n${message}`, }; errors.push(error); testErrors.push(error); continue; } for (const { emoji, text } of [ { emoji: "✓", text: "pass" }, { emoji: "✗", text: "fail" }, { emoji: "»", text: "skip" }, { emoji: "✎", text: "todo" }, ]) { if (!string.startsWith(emoji)) { continue; } const eol = string.lastIndexOf(" [") || undefined; const test = string.substring(1 + emoji.length, eol); const duration = eol ? string.substring(eol + 2, string.lastIndexOf("]")) : undefined; tests.push({ url: getFileUrl(testPath), file: testPath, test, status: text, errors: testErrors, duration: parseDuration(duration), }); for (let error of testErrors) { error.test = test; } testErrors = []; } } return { tests, errors, stdout: lines.join("\n"), }; } /** * @param {string} execPath * @param {SpawnOptions} options * @returns {Promise} */ async function spawnBunInstall(execPath, options) { const { ok, error, stdout, duration } = await spawnBun(execPath, { args: ["install"], timeout: isWindows ? hardTestTimeout : softTestTimeout, ...options, }); const relativePath = relative(cwd, options.cwd); const testPath = join(relativePath, "package.json"); const status = ok ? "pass" : "fail"; return { testPath, ok, status, error, errors: [], tests: [ { file: testPath, test: "bun install", status, duration: parseDuration(duration), }, ], stdout, }; } /** * @returns {string | undefined} */ function getGitSha() { const sha = process.env["GITHUB_SHA"] || process.env["BUILDKITE_COMMIT"]; if (sha?.length === 40) { return sha; } try { const { stdout } = spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf-8", timeout: spawnTimeout, }); return stdout.trim(); } catch (error) { console.warn(error); } } /** * @returns {string} */ function getGitRef() { const ref = process.env["GITHUB_REF_NAME"] || process.env["BUILDKITE_BRANCH"]; if (ref) { return ref; } try { const { stdout } = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf-8", timeout: spawnTimeout, }); return stdout.trim(); } catch (error) { console.warn(error); return ""; } } /** * @returns {string} */ function getTmpdir() { if (isMacOS) { if (existsSync("/tmp")) { return "/tmp"; } } if (isWindows) { for (const key of ["TMPDIR", "TEMP", "TEMPDIR", "TMP"]) { const tmpdir = process.env[key] || ""; if (!/^\/[a-zA-Z]\//.test(tmpdir)) { continue; } const driveLetter = tmpdir[1]; return normalizeWindows(`${driveLetter.toUpperCase()}:${tmpdir.substring(2)}`); } } return tmpdir(); } /** * @param {string} path * @returns {boolean} */ function isJavaScript(path) { return /\.(c|m)?(j|t)sx?$/.test(basename(path)); } /** * @param {string} path * @returns {boolean} */ function isTest(path) { return isJavaScript(path) && /\.test|spec\./.test(basename(path)); } /** * @param {string} path * @returns {boolean} */ function isSequentialTest(path) { if (/\/(integration|io|net|spawn|shell|socket|tcp|udp|dgram|http|http2|server|listen|fs|fetch)\//.test(path)) { return true; } if (/stress|bench|leak/.test(path)) { return true; } return false; } /** * @param {string} path * @returns {boolean} */ function isHidden(path) { return /node_modules|node.js/.test(dirname(path)) || /^\./.test(basename(path)); } /** * @param {string} cwd * @returns {string[]} */ function getTests(cwd) { function* getFiles(cwd, path) { const dirname = join(cwd, path); for (const entry of readdirSync(dirname, { encoding: "utf-8", withFileTypes: true })) { const { name } = entry; const filename = join(path, name); if (isHidden(filename)) { continue; } if (entry.isFile() && isTest(filename)) { yield filename; } else if (entry.isDirectory()) { yield* getFiles(cwd, filename); } } } return [...getFiles(cwd, "")].sort(); } let ntStatus; /** * @param {number} exitCode * @returns {string} */ function getWindowsExitCode(exitCode) { if (ntStatus === undefined) { const ntStatusPath = "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.22621.0\\shared\\ntstatus.h"; try { ntStatus = readFileSync(ntStatusPath, "utf-8"); } catch (error) { console.warn(error); ntStatus = ""; } } const match = ntStatus.match(new RegExp(`(STATUS_\\w+).*0x${exitCode?.toString(16)}`, "i")); return match?.[1]; } /** * @param {string} bunExe * @returns {string} */ function getExecPath(bunExe) { let execPath; let error; try { const { error, stdout } = spawnSync(bunExe, ["--print", "process.argv[0]"], { encoding: "utf-8", timeout: spawnTimeout, env: { PATH: process.env.PATH, BUN_DEBUG_QUIET_LOGS: 1, }, }); if (error) { throw error; } execPath = stdout.trim(); } catch (cause) { error = cause; } if (execPath) { if (isExecutable(execPath)) { return execPath; } error = new Error(`File is not an executable: ${execPath}`); } throw new Error(`Could not find executable: ${bunExe}`, { cause: error }); } /** * @param {string} target * @returns {Promise} */ async function getExecPathFromBuildKite(target) { const releasePath = join(cwd, "release"); mkdirSync(releasePath, { recursive: true }); await spawnSafe({ command: "buildkite-agent", args: ["artifact", "download", "**", releasePath, "--step", target], }); const zipPath = join(releasePath, `${target}.zip`); if (isWindows) { await spawnSafe({ command: "powershell", args: ["-Command", `Expand-Archive -Path ${zipPath} -DestinationPath ${releasePath}`], }); } else { await spawnSafe({ command: "unzip", args: ["-o", zipPath, "-d", releasePath], }); } const execPath = join(releasePath, target, isWindows ? "bun.exe" : "bun"); if (!isExecutable(execPath)) { throw new Error(`Could not find executable from BuildKite: ${execPath}`); } return execPath; } /** * @param {string} execPath * @returns {string} */ function getRevision(execPath) { try { const { error, stdout } = spawnSync(execPath, ["--revision"], { encoding: "utf-8", timeout: spawnTimeout, env: { PATH: process.env.PATH, BUN_DEBUG_QUIET_LOGS: 1, }, }); if (error) { throw error; } return stdout.trim(); } catch (error) { console.warn(error); return ""; } } /** * @returns {string} */ function getOsText() { const { platform } = process; switch (platform) { case "darwin": return "darwin"; case "linux": return "linux"; case "win32": return "windows"; default: return platform; } } /** * @returns {string} */ function getOsPrettyText() { const { platform } = process; if (platform === "darwin") { const properties = {}; for (const property of ["productName", "productVersion", "buildVersion"]) { try { const { error, stdout } = spawnSync("sw_vers", [`-${property}`], { encoding: "utf-8", timeout: spawnTimeout, env: { PATH: process.env.PATH, }, }); if (error) { throw error; } properties[property] = stdout.trim(); } catch (error) { console.warn(error); } } const { productName, productVersion, buildVersion } = properties; if (!productName) { return "macOS"; } if (!productVersion) { return productName; } if (!buildVersion) { return `${productName} ${productVersion}`; } return `${productName} ${productVersion} (build: ${buildVersion})`; } if (platform === "linux") { try { const { error, stdout } = spawnSync("lsb_release", ["--description", "--short"], { encoding: "utf-8", timeout: spawnTimeout, env: { PATH: process.env.PATH, }, }); if (error) { throw error; } return stdout.trim(); } catch (error) { console.warn(error); return "Linux"; } } if (platform === "win32") { try { const { error, stdout } = spawnSync("cmd", ["/c", "ver"], { encoding: "utf-8", timeout: spawnTimeout, env: { PATH: process.env.PATH, }, }); if (error) { throw error; } return stdout.trim(); } catch (error) { console.warn(error); return "Windows"; } } return platform; } /** * @returns {string} */ function getOsEmoji() { const { platform } = process; switch (platform) { case "darwin": return isBuildKite ? ":apple:" : ""; case "win32": return isBuildKite ? ":windows:" : "🪟"; case "linux": return isBuildKite ? ":linux:" : "🐧"; default: return "🔮"; } } /** * @returns {string} */ function getArchText() { const { arch } = process; switch (arch) { case "x64": return "x64"; case "arm64": return "aarch64"; default: return arch; } } /** * @returns {string} */ function getArchEmoji() { const { arch } = process; switch (arch) { case "x64": return "🖥"; case "arm64": return "💪"; default: return "🔮"; } } /** * @returns {string | undefined} */ function getBuildUrl() { if (isBuildKite) { const buildUrl = process.env["BUILDKITE_BUILD_URL"]; const jobId = process.env["BUILDKITE_JOB_ID"]; if (buildUrl) { return jobId ? `${buildUrl}#${jobId}` : buildUrl; } } if (isGitHubAction) { const baseUrl = process.env["GITHUB_SERVER_URL"]; const repository = process.env["GITHUB_REPOSITORY"]; const runId = process.env["GITHUB_RUN_ID"]; if (baseUrl && repository && runId) { return `${baseUrl}/${repository}/actions/runs/${runId}`; } } } /** * @returns {string} */ function getBuildLabel() { if (isBuildKite) { return process.env["BUILDKITE_GROUP_LABEL"] || process.env["BUILDKITE_LABEL"]; } return `${getOsEmoji()} ${getArchEmoji()}`; } /** * @param {string} file * @param {number} [line] * @returns {string | undefined} */ function getFileUrl(file, line) { const filePath = file.replace(/\\/g, "/"); let url; if (pullRequest) { const fileMd5 = crypto.createHash("md5").update(filePath).digest("hex"); url = `${baseUrl}/${repository}/pull/${pullRequest}/files#diff-${fileMd5}`; if (line !== undefined) { url += `L${line}`; } } else if (gitSha) { url = `${baseUrl}/${repository}/blob/${gitSha}/${filePath}`; if (line !== undefined) { url += `#L${line}`; } } return url; } /** * @returns {string | undefined} */ function getCI() { if (isBuildKite) { return "BuildKite"; } if (isGitHubAction) { return "GitHub Actions"; } if (isCI) { return "CI"; } } /** * @returns {string | undefined} */ function getHostname() { if (isBuildKite) { return process.env["BUILDKITE_AGENT_NAME"]; } try { return hostname(); } catch (error) { console.warn(error); } } /** * @param {...string} paths * @returns {string} */ function addPath(...paths) { if (isWindows) { return paths.join(";"); } return paths.join(":"); } /** * @param {string} title * @param {function} fn */ async function runTask(title, fn) { if (isGitHubAction) { console.log(`::group::${stripAnsi(title)}`); } else if (isBuildKite) { console.log(`--- ${title}`); } else { console.log(title); } try { return await fn(); } catch (error) { console.error(error); process.exit(1); } finally { if (isGitHubAction) { console.log("::endgroup::"); } console.log(); } } /** * @param {...TestResult} results * @returns {string} */ function formatTestToMarkdown(...results) { const buildLabel = getBuildLabel(); const buildUrl = getBuildUrl(); const platform = buildUrl ? `${buildLabel}` : buildLabel; let markdown = ""; for (const { testPath, ok, tests, error, stdout } of results) { if (ok) { continue; } let errorLine; for (const { error } of tests) { if (!error) { continue; } const { file, line } = error; if (line) { errorLine = line; break; } } const testTitle = testPath.replace(/\//g, "/"); const testUrl = getFileUrl(testPath, errorLine); markdown += "
"; if (testUrl) { markdown += `${testTitle}`; } else { markdown += `${testTitle}`; } if (error) { markdown += ` - ${error}`; } markdown += ` on ${platform}\n\n`; if (isBuildKite) { const preview = escapeCodeBlock(stdout); markdown += `\`\`\`terminal\n${preview}\n\`\`\`\n`; } else { const preview = escapeHtml(stripAnsi(stdout)); markdown += `
${preview}
\n`; } markdown += `\n\n
\n\n`; } return markdown; } /** * @param {string} label * @param {string} content * @param {number | undefined} attempt */ function reportAnnotationToBuildKite(label, content, attempt = 0) { const { error, status, signal, stderr } = spawnSync( "buildkite-agent", ["annotate", "--append", "--style", "error", "--context", `${label}`], { input: content, stdio: ["pipe", "ignore", "pipe"], timeout: spawnTimeout, cwd, }, ); if (status === 0) { return; } if (attempt > 0) { console.error("Failed to create annotation:", { label, error, status, signal }); process.exit(1); } const cause = signal ?? `code ${status}`; const preview = `\`\`\`terminal\n${escapeCodeBlock(stderr)}\n\`\`\``; const message = `Failed to create annotation for \`${label}\`: ${cause}\n${preview}`; reportAnnotationToBuildKite(`${label}-error`, message, attempt + 1); } /** * @param {string} color * @returns {string} */ function getAnsi(color) { switch (color) { case "red": return "\x1b[31m"; case "green": return "\x1b[32m"; case "yellow": return "\x1b[33m"; case "blue": return "\x1b[34m"; case "reset": return "\x1b[0m"; case "gray": return "\x1b[90m"; default: return ""; } } /** * @param {string} string * @returns {string} */ function stripAnsi(string) { return string.replace(/\u001b\[\d+m/g, ""); } /** * @param {string} string * @returns {string} */ function escapeGitHubAction(string) { return string.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A"); } /** * @param {string} string * @returns {string} */ function unescapeGitHubAction(string) { return string.replace(/%25/g, "%").replace(/%0D/g, "\r").replace(/%0A/g, "\n"); } /** * @param {string} string * @returns {string} */ function escapeHtml(string) { return string .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") .replace(/`/g, "`"); } /** * @param {string} string * @returns {string} */ function escapeCodeBlock(string) { return string.replace(/`/g, "\\`"); } /** * @param {string} string * @returns {number | undefined} */ function parseDuration(duration) { const match = /(\d+\.\d+)(m?s)/.exec(duration); if (!match) { return undefined; } const [, value, unit] = match; return parseFloat(value) * (unit === "ms" ? 1 : 1000); } /** * @param {string} status * @returns {string} */ function getTestEmoji(status) { switch (status) { case "pass": return "✅"; case "fail": return "❌"; case "skip": return "⏭"; case "todo": return "✏️"; default: return "🔮"; } } /** * @param {string} status * @returns {string} */ function getTestColor(status) { switch (status) { case "pass": return getAnsi("green"); case "fail": return getAnsi("red"); case "skip": case "todo": default: return getAnsi("gray"); } } /** * @param {string} execPath * @returns {boolean} */ function isExecutable(execPath) { if (!existsSync(execPath) || !statSync(execPath).isFile()) { return false; } try { accessSync(execPath, fs.X_OK); } catch { return false; } return true; } const [target] = process.argv.slice(2); if (!target) { const filename = relative(cwd, import.meta.filename); throw new Error(`Usage: ${process.argv0} ${filename} `); } runTask("Environment", printInfo); const exitCode = await runTests(target); process.exit(exitCode);