diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/bun-test.ts | 681 | ||||
| -rw-r--r-- | scripts/get-revision.js | 1 | ||||
| -rw-r--r-- | scripts/package.json | 3 |
3 files changed, 0 insertions, 685 deletions
diff --git a/scripts/bun-test.ts b/scripts/bun-test.ts deleted file mode 100644 index 1aaef2b31..000000000 --- a/scripts/bun-test.ts +++ /dev/null @@ -1,681 +0,0 @@ -import { join, basename } from "node:path"; -import { readdirSync, writeSync, fsyncSync, appendFileSync } from "node:fs"; -import { spawn, spawnSync } from "node:child_process"; - -export { parseTest, runTest, formatTest }; - -export type ParseTestOptions = { - cwd?: string; - paths?: string[]; -}; - -export type ParseTestResult = { - info: TestInfo; - files: TestFile[]; - summary: TestSummary; -}; - -export type RunTestOptions = ParseTestOptions & { - args?: string[]; - timeout?: number; - isolate?: boolean; -}; - -export type RunTestResult = ParseTestResult & { - exitCode: number | null; - stdout: string; - stderr: string; -}; - -export type TestInfo = { - name: string; - version: string; - revision: string; - os: string; - arch: string; -}; - -export type TestFile = { - file: string; - status: TestStatus; - tests: Test[]; - summary: TestSummary; - errors?: TestError[]; -}; - -export type TestError = { - name: string; - message: string; - stack?: TestErrorStack[]; -}; - -export type TestErrorStack = { - file: string; - function?: string; - line: number; - column?: number; -}; - -export type TestStatus = "pass" | "fail" | "skip"; - -export type Test = { - name: string; - status: TestStatus; - errors?: TestError[]; -}; - -export type TestSummary = { - pass: number; - fail: number; - skip: number; - tests: number; - files: number; - duration: number; -}; - -function parseTest(lines: string[], options?: ParseTestOptions): ParseTestResult { - let i = 0; - const isDone = () => { - return i >= lines.length; - }; - const readLine = () => { - return lines[i++]; - }; - function readUntil<V>(cb: (line: string) => V | undefined): V | undefined { - while (!isDone()) { - const line = readLine(); - const result = cb(line); - if (result) { - return result; - } - } - } - const { cwd, paths = cwd ? Array.from(listFiles(cwd, "")) : [] } = options ?? {}; - const info = readUntil(parseInfo); - if (!info) { - throw new Error("No tests found"); - } - const files: TestFile[] = []; - let file: TestFile | undefined; - let test: Test | undefined; - let error: TestError | undefined; - let errorStart: number | undefined; - let summary: TestSummary | undefined; - const reset = () => { - if (error) { - if (file) { - if (test) { - if (!test?.errors) { - test.errors = [error]; - } else { - test.errors.push(error); - } - } else { - if (!file.errors) { - file.errors = [error]; - } else { - file.errors.push(error); - } - } - } - error = undefined; - errorStart = undefined; - } - }; - while (!isDone()) { - const line = readLine(); - if (error) { - const newStack = parseStack(line, cwd); - if (newStack) { - if (errorStart !== undefined) { - for (let j = errorStart; j < i - 1; j++) { - error.message += `\n${lines[j]}`; - } - errorStart = undefined; - } - if (!error.stack) { - error.stack = [newStack]; - } else { - error.stack.push(newStack); - } - continue; - } - } - const newFile = parseFile(line, paths); - if (newFile) { - reset(); - file = newFile; - files.push(file); - continue; - } - const newTest = parseStatus(line); - if (newTest) { - if (newTest.status === "skip" && error) { - continue; - } - test = newTest; - if (file) { - file.tests.push(test); - file.summary[test.status]++; - file.summary.tests++; - if (test.status === "fail") { - file.status = "fail"; - } - } - file?.tests.push(test); - reset(); - continue; - } - const newError = parseError(line); - if (newError) { - reset(); - error = newError; - errorStart = i; - continue; - } - const newSkip = parseSkip(line); - if (newSkip) { - reset(); - i += parseSkip(line); - continue; - } - const newSummary = parseSummary(line); - if (newSummary) { - summary = newSummary; - break; - } - } - summary = { - tests: files.reduce((n, file) => n + file.tests.length, 0), - files: files.length, - duration: 0, - ...summary, - pass: files.reduce((n, file) => n + file.tests.filter(test => test.status === "pass").length, 0), - fail: files.reduce((n, file) => n + file.tests.filter(test => test.status === "fail").length, 0), - skip: files.reduce((n, file) => n + file.tests.filter(test => test.status === "skip").length, 0), - }; - if (files.length === 1) { - files[0].summary = summary; - } - return { - info, - files, - summary, - }; -} - -function getRevision(): string { - if ("Bun" in globalThis) { - return Bun.revision; - } - const { stdout } = spawnSync("bun", ["get-revision.js"], { - cwd: new URL(".", import.meta.url), - stdio: "pipe", - encoding: "utf-8", - }); - return stdout.trim(); -} - -function parseInfo(line: string): TestInfo | undefined { - const match = /^(bun (?:wip)?test) v([0-9\.]+) \(([0-9a-z]+)\)$/.exec(line); - if (!match) { - return undefined; - } - const [, name, version, sha] = match; - return { - name, - version, - revision: getRevision(), - os: process.platform, - arch: process.arch, - }; -} - -function parseFile(line: string, paths?: string[]): TestFile | undefined { - const match = /^([a-z0-9_-]+\.(?:test|spec)\.(?:c|m)?(?:j|t)sx?)\:$/.exec(line); - if (!match) { - return undefined; - } - let [, file] = match; - for (const path of paths ?? []) { - if (path.endsWith(file)) { - file = path; - break; - } - } - return { - file, - tests: [], - status: "pass", - summary: { - files: 1, - tests: 0, - pass: 0, - fail: 0, - skip: 0, - duration: 0, - }, - }; -} - -function parseStatus(line: string): Test | undefined { - const match = /^(✓|✗|-) (.*)$/.exec(line); - if (!match) { - return undefined; - } - const [, icon, name] = match; - return { - name, - status: icon === "✓" ? "pass" : icon === "✗" ? "fail" : "skip", - }; -} - -function parseError(line: string): TestError | undefined { - const match = /^(.*error)\: (.*)$/i.exec(line); - if (!match) { - return undefined; - } - const [, name, message] = match; - return { - name: name === "error" ? "Error" : name, - message, - }; -} - -function parseStack(line: string, cwd?: string): TestErrorStack | undefined { - let match = /^\s*at (.*) \((.*)\:([0-9]+)\:([0-9]+)\)$/.exec(line); - if (!match) { - match = /^\s*at (.*)\:([0-9]+)\:([0-9]+)$/.exec(line); - if (!match) { - return undefined; - } - } - const [columnNo, lineNo, path, func] = match.reverse(); - let file = path; - if (cwd && path.startsWith(cwd)) { - file = path.slice(cwd.length); - if (file.startsWith("/")) { - file = file.slice(1); - } - } - return { - file, - function: func !== line ? func : undefined, - line: parseInt(lineNo), - column: parseInt(columnNo), - }; -} - -function parseSkip(line: string): number { - const match = /^([0-9]+) tests (?:skipped|failed)\:$/.exec(line); - if (match) { - return parseInt(match[1]); - } - return 0; -} - -function parseSummary(line: string): TestSummary | undefined { - const match = /^Ran ([0-9]+) tests across ([0-9]+) files \[([0-9]+\.[0-9]+)(m?s)\]$/.exec(line); - if (!match) { - return undefined; - } - const [, tests, files, duration, unit] = match; - return { - pass: 0, - fail: 0, - skip: 0, - tests: parseInt(tests), - files: parseInt(files), - duration: parseFloat(duration) * (unit === "s" ? 1000 : 1), - }; -} - -function* listFiles(cwd: string, dir: string): Generator<string> { - const dirents = readdirSync(join(cwd, dir), { withFileTypes: true }); - for (const dirent of dirents) { - const { name } = dirent; - if (name === "node_modules" || name.startsWith(".")) { - continue; - } - const path = join(dir, name); - if (dirent.isDirectory()) { - yield* listFiles(cwd, path); - } else if (dirent.isFile()) { - yield path; - } - } -} - -function stripAnsi(string: string): string { - return string.replace(/\x1b\[[0-9;]*m/g, ""); -} - -function print(buffer: string | Uint8Array) { - if (typeof buffer === "string") { - buffer = new TextEncoder().encode(buffer); - } - let offset = 0; - let length = buffer.byteLength; - while (offset < length) { - try { - const n = writeSync(1, buffer); - offset += n; - if (offset < length) { - try { - fsyncSync(1); - } catch {} - buffer = buffer.slice(n); - } - } catch (error) { - // @ts-ignore - if (error.code === "EAGAIN") { - continue; - } - throw error; - } - } -} - -// FIXME: there is a bug that causes annotations to be duplicated -const seen = new Set<string>(); - -function annotate(type: string, arg?: string, args?: Record<string, unknown>): void { - let line = `::${type}`; - if (args) { - line += " "; - line += Object.entries(args) - .map(([key, value]) => `${key}=${value}`) - .join(","); - } - line += "::"; - if (arg) { - line += arg; - } - line = line.replace(/\n/g, "%0A"); - if (seen.has(line)) { - return; - } - seen.add(line); - print(`\n${line}\n`); -} - -async function* runTest(options: RunTestOptions): AsyncGenerator<RunTestResult, ParseTestResult> { - const { - cwd = process.cwd(), - args = [], - timeout = 60_000, // 1 min - isolate = false, - } = options; - const paths: string[] = Array.from(listFiles(cwd, "")); - const files: string[] = []; - for (const path of paths) { - if (!path.includes(".test.")) { - continue; - } - if (!args.length) { - files.push(path); - continue; - } - for (const arg of args) { - if ( - (arg.endsWith("/") && path.startsWith(arg)) || - (arg.includes(".") && path.endsWith(arg)) || - (!arg.endsWith("/") && !arg.includes(".") && path.includes(arg)) - ) { - files.push(path); - break; - } - } - } - const runSingleTest = async (args: string[]) => { - const runner = spawn("bun", ["test", ...args], { - cwd, - env: { - ...process.env, - "FORCE_COLOR": "1", - }, - stdio: "pipe", - timeout, - }); - let stderr = ""; - let stdout = ""; - const exitCode = await new Promise<number | null>(resolve => { - runner.stdout.on("data", (data: Buffer) => { - stdout += data.toString("utf-8"); - }); - runner.stderr.on("data", (data: Buffer) => { - stderr += data.toString("utf-8"); - }); - runner.on("error", ({ name, message }) => { - stderr += `${name}: ${message}`; - resolve(null); - }); - runner.on("exit", exitCode => { - resolve(exitCode); - }); - }); - const lines = stderr.split("\n").map(stripAnsi); - const result = parseTest(lines, { cwd, paths }); - return { - exitCode, - stdout, - stderr, - ...result, - }; - }; - if (!isolate) { - const result = await runSingleTest(args); - yield result; - return result; - } - const tests = files.map(file => runSingleTest([file])); - const results: RunTestResult[] = []; - for (const test of tests) { - const result = await test; - results.push(result); - yield result; - } - if (!results.length) { - throw new Error("No tests found"); - } - return { - info: results.map(result => result.info).pop()!, - files: results.flatMap(result => result.files), - summary: results - .map(result => result.summary) - .reduce((summary, result) => { - summary.pass += result.pass; - summary.fail += result.fail; - summary.skip += result.skip; - summary.tests += result.tests; - summary.files += result.files; - summary.duration += result.duration; - return summary; - }), - }; -} - -export type FormatTestOptions = { - debug?: boolean; - baseUrl?: string; -}; - -function formatTest(result: ParseTestResult, options?: FormatTestOptions): string { - const { debug, baseUrl } = options ?? {}; - const count = (n: number, label?: string) => { - return n ? (label ? `${n} ${label}` : `${n}`) : ""; - }; - const code = (content: string, lang?: string) => { - return `\`\`\`${lang ?? ""}\n${content}\n\`\`\`\n`; - }; - const link = (title: string, href?: string) => { - if (href && baseUrl) { - href = `${new URL(href, baseUrl)}`; - } - return href ? `[${title}](${href})` : title; - }; - const table = (headers: string[], rows: unknown[][]) => { - return [headers, headers.map(() => "-"), ...rows].map(row => `| ${row.join(" | ")} |`).join("\n"); - }; - const header = (level: number, content: string) => { - return `${"#".repeat(level)} ${content}\n`; - }; - const icon = { - pass: "✅", - fail: "❌", - skip: "⏭️", - }; - const files = table( - ["File", "Status", "Pass", "Fail", "Skip", "Tests", "Duration"], - result.files - .filter(({ status }) => debug || status !== "pass") - .sort((a, b) => { - if (a.status === b.status) { - return a.file.localeCompare(b.file); - } - return a.status.localeCompare(b.status); - }) - .map(({ file, status, summary }) => [ - link(basename(file), file), - icon[status], - count(summary.pass), - count(summary.fail), - count(summary.skip), - count(summary.tests), - count(summary.duration, "ms"), - ]), - ); - const errors = result.files - .filter(({ status }) => status === "fail") - .sort((a, b) => a.file.localeCompare(b.file)) - .flatMap(({ file, tests }) => { - return [ - header(2, link(basename(file), file)), - ...tests - .filter(({ status }) => status === "fail") - .map(({ name, errors }) => { - let content = header(3, name); - if (errors) { - content += errors - .map(({ name, message, stack }) => { - let preview = code(`${name}: ${message}`, "diff"); - if (stack?.length && baseUrl) { - const { file, line } = stack[0]; - if (!is3rdParty(file)) { - const { href } = new URL(`${file}?plain=1#L${Math.max(1, line - 5)}-L${line}`, baseUrl); - preview += `\n${href}\n`; - } - } - return preview; - }) - .join("\n"); - } else { - content += code("See logs for details"); - } - return content; - }), - ]; - }) - .join("\n"); - return `${header(1, "Files")} -${files} - -${header(1, "Errors")} -${errors}`; -} - -function is3rdParty(file?: string): boolean { - return !file || file.startsWith("/") || file.includes(":") || file.includes("..") || file.includes("node_modules/"); -} - -function printTest(result: RunTestResult): void { - const isAction = process.env["GITHUB_ACTIONS"] === "true"; - const isSingle = result.files.length === 1; - if (isSingle) { - const { file, status } = result.files[0]; - if (isAction) { - annotate("group", `${status.toUpperCase()} - ${file}`); - } else { - print(`\n${file}:\n`); - } - } - print(result.stderr); - print(result.stdout); - if (!isAction) { - return; - } - result.files - .filter(({ status }) => status === "fail") - .flatMap(({ tests }) => tests) - .filter(({ status }) => status === "fail") - .flatMap(({ name: title, errors }) => - errors?.forEach(({ name, message, stack }) => { - const { file, line } = stack?.[0] ?? {}; - if (is3rdParty(file)) { - return; - } - annotate("error", `${name}: ${message}`, { - file, - line, - title, - }); - }), - ); - if (isSingle) { - annotate("endgroup"); - } -} - -async function main() { - let args = [...process.argv.slice(2)]; - let timeout; - let isolate; - let quiet; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (arg.startsWith("--timeout=")) { - timeout = parseInt(arg.split("=").pop()!); - } else if (arg.startsWith("--isolate")) { - isolate = true; - } else if (arg.startsWith("--quiet")) { - quiet = true; - } - } - args = args.filter(arg => !arg.startsWith("--")); - const results = runTest({ - args, - timeout, - isolate, - }); - let result: ParseTestResult; - while (true) { - const { value, done } = await results.next(); - if (done) { - result = value; - break; - } else if (!quiet) { - printTest(value); - } - } - const summaryPath = process.env["GITHUB_STEP_SUMMARY"]; - if (!summaryPath) { - return; - } - const sha = process.env["GITHUB_SHA"] ?? result.info.revision; - const repo = process.env["GITHUB_REPOSITORY"] ?? "oven-sh/bun"; - const serverUrl = process.env["GITHUB_SERVER_URL"] ?? "https://github.com"; - const summary = formatTest(result, { - debug: process.env["ACTIONS_STEP_DEBUG"] === "true", - baseUrl: `${serverUrl}/${repo}/blob/${sha}/`, - }); - appendFileSync(summaryPath, summary, "utf-8"); - process.exit(0); -} - -function isMain() { - return import.meta.main || import.meta.url === `file://${process.argv[1]}`; -} - -if (isMain()) { - await main(); -} diff --git a/scripts/get-revision.js b/scripts/get-revision.js deleted file mode 100644 index 09905e5bf..000000000 --- a/scripts/get-revision.js +++ /dev/null @@ -1 +0,0 @@ -console.log(Bun.revision); diff --git a/scripts/package.json b/scripts/package.json deleted file mode 100644 index 3dbc1ca59..000000000 --- a/scripts/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} |
