mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 05:42:43 +00:00
607 lines
17 KiB
JavaScript
607 lines
17 KiB
JavaScript
import * as action from "@actions/core";
|
|
import { spawn, spawnSync } from "child_process";
|
|
import { closeSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
import { readdirSync } from "node:fs";
|
|
import { basename, resolve } from "node:path";
|
|
import { cpus, hostname, tmpdir, totalmem, userInfo } from "os";
|
|
import PQueue from "p-queue";
|
|
import { join, normalize, posix, relative } from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const run_start = new Date();
|
|
const TIMEOUT_DURATION = 1000 * 60 * 5;
|
|
const SHORT_TIMEOUT_DURATION = Math.ceil(TIMEOUT_DURATION / 5);
|
|
|
|
function defaultConcurrency() {
|
|
// This causes instability due to the number of open file descriptors / sockets in some tests
|
|
// Windows has higher limits
|
|
if (process.platform !== "win32") {
|
|
return 1;
|
|
}
|
|
|
|
return Math.min(Math.floor((cpus().length - 2) / 2), 2);
|
|
}
|
|
const windows = process.platform === "win32";
|
|
const nativeMemory = totalmem();
|
|
const force_ram_size_input = parseInt(process.env["BUN_JSC_forceRAMSize"] || "0", 10);
|
|
let force_ram_size = Number(BigInt(nativeMemory) >> BigInt(2)) + "";
|
|
if (!(Number.isSafeInteger(force_ram_size_input) && force_ram_size_input > 0)) {
|
|
force_ram_size = force_ram_size_input + "";
|
|
}
|
|
function uncygwinTempDir() {
|
|
if (process.platform === "win32") {
|
|
for (let key of ["TMPDIR", "TEMP", "TEMPDIR", "TMP"]) {
|
|
let TMPDIR = process.env[key] || "";
|
|
if (!/^\/[a-zA-Z]\//.test(TMPDIR)) {
|
|
continue;
|
|
}
|
|
|
|
const driveLetter = TMPDIR[1];
|
|
TMPDIR = path.win32.normalize(`${driveLetter.toUpperCase()}:` + TMPDIR.substring(2));
|
|
process.env[key] = TMPDIR;
|
|
}
|
|
}
|
|
}
|
|
|
|
uncygwinTempDir();
|
|
|
|
const cwd = resolve(fileURLToPath(import.meta.url), "../../../../");
|
|
process.chdir(cwd);
|
|
|
|
const ci = !!process.env["GITHUB_ACTIONS"];
|
|
const enableProgressBar = false;
|
|
|
|
const dirPrefix = "bun-test-tmp-" + ((Math.random() * 100_000_0) | 0).toString(36) + "_";
|
|
const run_concurrency = Math.max(Number(process.env["BUN_TEST_CONCURRENCY"] || defaultConcurrency(), 10), 1);
|
|
const queue = new PQueue({ concurrency: run_concurrency });
|
|
|
|
var prevTmpdir = "";
|
|
function maketemp() {
|
|
prevTmpdir = join(
|
|
tmpdir(),
|
|
dirPrefix + (Date.now() | 0).toString() + "_" + ((Math.random() * 100_000_0) | 0).toString(36),
|
|
);
|
|
mkdirSync(prevTmpdir, { recursive: true });
|
|
return prevTmpdir;
|
|
}
|
|
|
|
const extensions = [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs", ".mts", ".cts", ".mjsx", ".cjsx", ".mtsx", ".ctsx"];
|
|
|
|
const git_sha =
|
|
process.env["GITHUB_SHA"] ?? spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf-8" }).stdout.trim();
|
|
|
|
const TEST_FILTER = process.env.BUN_TEST_FILTER;
|
|
|
|
function isTest(path) {
|
|
if (!basename(path).includes(".test.") || !extensions.some(ext => path.endsWith(ext))) {
|
|
return false;
|
|
}
|
|
|
|
if (TEST_FILTER) {
|
|
if (!path.includes(TEST_FILTER)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function* findTests(dir, query) {
|
|
for (const entry of readdirSync(resolve(dir), { encoding: "utf-8", withFileTypes: true })) {
|
|
const path = resolve(dir, entry.name);
|
|
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
|
|
yield* findTests(path, query);
|
|
} else if (isTest(path)) {
|
|
yield path;
|
|
}
|
|
}
|
|
}
|
|
|
|
let bunExe = "bun";
|
|
|
|
if (process.argv.length > 2) {
|
|
bunExe = resolve(process.argv.at(-1));
|
|
} else if (process.env.BUN_PATH) {
|
|
const { BUN_PATH_BASE, BUN_PATH } = process.env;
|
|
bunExe = resolve(normalize(BUN_PATH_BASE), normalize(BUN_PATH));
|
|
}
|
|
|
|
const { error, stdout: revision_stdout } = spawnSync(bunExe, ["--revision"], {
|
|
env: { ...process.env, BUN_DEBUG_QUIET_LOGS: 1 },
|
|
});
|
|
if (error) {
|
|
if (error.code !== "ENOENT") throw error;
|
|
console.error(`\x1b[31merror\x1b[0;2m:\x1b[0m Could not find Bun executable at '${bunExe}'`);
|
|
process.exit(1);
|
|
}
|
|
const revision = revision_stdout.toString().trim();
|
|
|
|
const { error: error2, stdout: argv0_stdout } = spawnSync(bunExe, ["-e", "console.log(process.argv[0])"], {
|
|
env: { ...process.env, BUN_DEBUG_QUIET_LOGS: 1 },
|
|
});
|
|
if (error2) throw error2;
|
|
const argv0 = argv0_stdout.toString().trim();
|
|
|
|
console.log(`Testing ${argv0} v${revision}`);
|
|
|
|
const ntStatusPath = "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.22621.0\\shared\\ntstatus.h";
|
|
let ntstatus_header_cache = null;
|
|
function lookupWindowsError(code) {
|
|
if (ntstatus_header_cache === null) {
|
|
try {
|
|
ntstatus_header_cache = readFileSync(ntStatusPath, "utf-8");
|
|
} catch {
|
|
console.error(`could not find ntstatus.h to lookup error code: ${ntStatusPath}`);
|
|
ntstatus_header_cache = "";
|
|
}
|
|
}
|
|
const match = ntstatus_header_cache.match(new RegExp(`(STATUS_\\w+).*0x${code.toString(16)}`, "i"));
|
|
if (match) {
|
|
return match[1];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const failing_tests = [];
|
|
const passing_tests = [];
|
|
let maxFd = -1;
|
|
function getMaxFileDescriptor(path) {
|
|
if (process.platform === "win32") {
|
|
return -1;
|
|
}
|
|
|
|
hasInitialMaxFD = true;
|
|
|
|
if (process.platform === "linux" || process.platform === "darwin") {
|
|
try {
|
|
readdirSync(process.platform === "darwin" ? "/dev/fd" : "/proc/self/fd").forEach(name => {
|
|
const fd = parseInt(name.trim(), 10);
|
|
if (Number.isSafeInteger(fd) && fd >= 0) {
|
|
maxFd = Math.max(maxFd, fd);
|
|
}
|
|
});
|
|
|
|
return maxFd;
|
|
} catch {}
|
|
}
|
|
|
|
const devnullfd = openSync("/dev/null", "r");
|
|
closeSync(devnullfd);
|
|
maxFd = devnullfd + 1;
|
|
return maxFd;
|
|
}
|
|
let hasInitialMaxFD = false;
|
|
|
|
const activeTests = new Map();
|
|
|
|
let slowTestCount = 0;
|
|
function checkSlowTests() {
|
|
const now = Date.now();
|
|
const prevSlowTestCount = slowTestCount;
|
|
slowTestCount = 0;
|
|
for (const [path, { start, proc }] of activeTests) {
|
|
if (proc && now - start >= TIMEOUT_DURATION) {
|
|
console.error(
|
|
`\x1b[31merror\x1b[0;2m:\x1b[0m Killing test ${JSON.stringify(path)} after ${Math.ceil((now - start) / 1000)}s`,
|
|
);
|
|
proc?.stdout?.destroy?.();
|
|
proc?.stderr?.destroy?.();
|
|
proc?.kill?.(9);
|
|
} else if (now - start > SHORT_TIMEOUT_DURATION) {
|
|
console.error(
|
|
`\x1b[33mwarning\x1b[0;2m:\x1b[0m Test ${JSON.stringify(path)} has been running for ${Math.ceil(
|
|
(now - start) / 1000,
|
|
)}s`,
|
|
);
|
|
slowTestCount++;
|
|
}
|
|
}
|
|
|
|
if (slowTestCount > prevSlowTestCount && queue.concurrency > 1) {
|
|
queue.concurrency += 1;
|
|
}
|
|
}
|
|
|
|
setInterval(checkSlowTests, SHORT_TIMEOUT_DURATION).unref();
|
|
var currentTestNumber = 0;
|
|
async function runTest(path) {
|
|
const pathOnDisk = resolve(path);
|
|
const thisTestNumber = currentTestNumber++;
|
|
const testFileName = posix.normalize(relative(cwd, path).replaceAll("\\", "/"));
|
|
let exitCode, signal, err, output;
|
|
|
|
const start = Date.now();
|
|
|
|
const activeTestObject = { start, proc: undefined };
|
|
activeTests.set(testFileName, activeTestObject);
|
|
|
|
try {
|
|
await new Promise((finish, reject) => {
|
|
const chunks = [];
|
|
process.stderr.write(
|
|
`
|
|
at ${((start - run_start.getTime()) / 1000).toFixed(2)}s, file ${thisTestNumber
|
|
.toString()
|
|
.padStart(total.toString().length, "0")}/${total}, ${failing_tests.length} failing files
|
|
Starting "${testFileName}"
|
|
|
|
`,
|
|
);
|
|
const TMPDIR = maketemp();
|
|
const proc = spawn(bunExe, ["test", pathOnDisk], {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
env: {
|
|
...process.env,
|
|
FORCE_COLOR: "1",
|
|
BUN_GARBAGE_COLLECTOR_LEVEL: "1",
|
|
BUN_JSC_forceRAMSize: force_ram_size,
|
|
BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0",
|
|
GITHUB_ACTIONS: process.env.GITHUB_ACTIONS ?? "true",
|
|
BUN_DEBUG_QUIET_LOGS: "1",
|
|
BUN_INSTALL_CACHE_DIR: join(TMPDIR, ".bun-install-cache"),
|
|
BUN_ENABLE_CRASH_REPORTING: "0",
|
|
[windows ? "TEMP" : "TMPDIR"]: TMPDIR,
|
|
},
|
|
});
|
|
activeTestObject.proc = proc;
|
|
proc.stdout.once("end", () => {
|
|
done();
|
|
});
|
|
|
|
let doneCalls = 0;
|
|
var done = () => {
|
|
// TODO: wait for stderr as well
|
|
// spawn.test currently causes it to hang
|
|
if (doneCalls++ === 1) {
|
|
actuallyDone();
|
|
}
|
|
};
|
|
var actuallyDone = function () {
|
|
actuallyDone = done = () => {};
|
|
proc?.stderr?.unref?.();
|
|
proc?.stdout?.unref?.();
|
|
proc?.unref?.();
|
|
output = Buffer.concat(chunks).toString();
|
|
finish();
|
|
};
|
|
|
|
// if (!KEEP_TMPDIR)
|
|
// proc.once("close", () => {
|
|
// rm(TMPDIR, { recursive: true, force: true }).catch(() => {});
|
|
// });
|
|
|
|
proc.stdout.on("data", chunk => {
|
|
chunks.push(chunk);
|
|
if (run_concurrency === 1) process.stdout.write(chunk);
|
|
});
|
|
proc.stderr.on("data", chunk => {
|
|
chunks.push(chunk);
|
|
if (run_concurrency === 1) process.stderr.write(chunk);
|
|
});
|
|
|
|
proc.once("close", () => {
|
|
activeTestObject.proc = undefined;
|
|
});
|
|
|
|
proc.once("exit", (code_, signal_) => {
|
|
activeTestObject.proc = undefined;
|
|
exitCode = code_;
|
|
signal = signal_;
|
|
if (signal || exitCode !== 0) {
|
|
actuallyDone();
|
|
} else {
|
|
done();
|
|
}
|
|
});
|
|
proc.once("error", err_ => {
|
|
activeTestObject.proc = undefined;
|
|
err = err_;
|
|
actuallyDone();
|
|
});
|
|
});
|
|
} finally {
|
|
activeTests.delete(testFileName);
|
|
}
|
|
|
|
if (!hasInitialMaxFD) {
|
|
getMaxFileDescriptor();
|
|
} else if (maxFd > 0) {
|
|
const prevMaxFd = maxFd;
|
|
maxFd = getMaxFileDescriptor();
|
|
if (maxFd > prevMaxFd + queue.concurrency * 2) {
|
|
process.stderr.write(
|
|
`\n\x1b[31mewarn\x1b[0;2m:\x1b[0m file descriptor leak in ${testFileName}, delta: ${
|
|
maxFd - prevMaxFd
|
|
}, current: ${maxFd}, previous: ${prevMaxFd}\n`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const passed = exitCode === 0 && !err && !signal;
|
|
|
|
let reason = "";
|
|
if (!passed) {
|
|
let match;
|
|
if (err && err.message.includes("timed")) {
|
|
reason = "hang";
|
|
} else if ((match = output && output.match(/thread \d+ panic: (.*)\n/))) {
|
|
reason = 'panic "' + match[1] + '"';
|
|
} else if (err) {
|
|
reason = (err.name || "Error") + ": " + err.message;
|
|
} else if (signal) {
|
|
reason = signal;
|
|
} else if (exitCode === 1) {
|
|
const failMatch = output.match(/\x1b\[31m\s(\d+) fail/);
|
|
if (failMatch) {
|
|
reason = failMatch[1] + " failing";
|
|
} else {
|
|
reason = "code 1";
|
|
}
|
|
} else {
|
|
const x = windows && lookupWindowsError(exitCode);
|
|
if (x) {
|
|
if (x === "STATUS_BREAKPOINT") {
|
|
if (output.includes("Segmentation fault at address")) {
|
|
reason = "STATUS_ACCESS_VIOLATION";
|
|
}
|
|
}
|
|
reason = x;
|
|
} else {
|
|
reason = "code " + exitCode;
|
|
}
|
|
}
|
|
}
|
|
|
|
const duration = (Date.now() - start) / 1000;
|
|
|
|
if (run_concurrency !== 1 && enableProgressBar) {
|
|
// clear line
|
|
process.stdout.write("\x1b[2K\r");
|
|
}
|
|
|
|
console.log(
|
|
`\x1b[2m${formatTime(duration).padStart(6, " ")}\x1b[0m ${
|
|
passed ? "\x1b[32m✔" : "\x1b[31m✖"
|
|
} ${testFileName}\x1b[0m${reason ? ` (${reason})` : ""}`,
|
|
);
|
|
|
|
finished++;
|
|
|
|
if (run_concurrency !== 1 && enableProgressBar) {
|
|
writeProgressBar();
|
|
}
|
|
|
|
if (run_concurrency > 1 && ci) {
|
|
process.stderr.write(output);
|
|
}
|
|
|
|
if (!passed) {
|
|
failing_tests.push({ path: testFileName, reason, output });
|
|
process.exitCode = 1;
|
|
if (err) console.error(err);
|
|
} else {
|
|
passing_tests.push(testFileName);
|
|
}
|
|
|
|
return passed;
|
|
}
|
|
|
|
var finished = 0;
|
|
|
|
function writeProgressBar() {
|
|
const barWidth = Math.min(process.stdout.columns || 40, 80) - 2;
|
|
const percent = (finished / total) * 100;
|
|
const bar = "=".repeat(Math.floor(percent / 2));
|
|
const str1 = `[${finished}/${total}] [${bar}`;
|
|
process.stdout.write(`\r${str1}${" ".repeat(barWidth - str1.length)}]`);
|
|
}
|
|
|
|
const allTests = [...findTests(resolve(cwd, "test"))];
|
|
console.log(`Starting ${allTests.length} tests with ${run_concurrency} concurrency...`);
|
|
let total = allTests.length;
|
|
for (const path of allTests) {
|
|
queue.add(
|
|
async () =>
|
|
await runTest(path).catch(e => {
|
|
console.error("Bug in bun-internal-test");
|
|
console.error(e);
|
|
process.exit(1);
|
|
}),
|
|
);
|
|
}
|
|
await queue.onIdle();
|
|
console.log(`
|
|
Completed ${total} tests with ${failing_tests.length} failing tests
|
|
`);
|
|
console.log("\n");
|
|
|
|
function linkToGH(linkTo) {
|
|
return `https://github.com/oven-sh/bun/blob/${git_sha}/${linkTo}`;
|
|
}
|
|
|
|
failing_tests.sort((a, b) => a.path.localeCompare(b.path));
|
|
passing_tests.sort((a, b) => a.localeCompare(b));
|
|
|
|
const failingTestDisplay = failing_tests.map(({ path, reason }) => `- \`${path}\` ${reason}`).join("\n");
|
|
|
|
// const passingTestDisplay = passing_tests.map(path => `- \`${path}\``).join("\n");
|
|
|
|
rmSync("report.md", { force: true });
|
|
|
|
const uptime = process.uptime();
|
|
|
|
function formatTime(seconds) {
|
|
if (seconds < 60) {
|
|
return seconds.toFixed(1) + "s";
|
|
} else if (seconds < 60 * 60) {
|
|
return (seconds / 60).toFixed(0) + "m " + formatTime(seconds % 60);
|
|
} else {
|
|
return (seconds / 60 / 60).toFixed(0) + "h " + formatTime(seconds % (60 * 60));
|
|
}
|
|
}
|
|
|
|
const header = `
|
|
host: ${process.env["GITHUB_RUN_ID"] ? "GitHub Actions: " : ""}${userInfo().username}@${hostname()}
|
|
platform: ${process.platform} ${process.arch}
|
|
bun: ${argv0}
|
|
version: v${revision}
|
|
|
|
date: ${run_start.toISOString()}
|
|
duration: ${formatTime(uptime)}
|
|
|
|
total: ${total} files
|
|
failing: ${failing_tests.length} files
|
|
passing: ${passing_tests.length} files
|
|
|
|
percent: ${((passing_tests.length / total) * 100).toFixed(2)}%
|
|
`.trim();
|
|
|
|
console.log("\n" + "-".repeat(Math.min(process.stdout.columns || 40, 80)) + "\n");
|
|
console.log(header);
|
|
console.log("\n" + "-".repeat(Math.min(process.stdout.columns || 40, 80)) + "\n");
|
|
|
|
let report = `# bun test on ${
|
|
process.env["GITHUB_REF"] ??
|
|
spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf-8" }).stdout.trim()
|
|
}
|
|
|
|
\`\`\`
|
|
${header}
|
|
\`\`\`
|
|
|
|
`;
|
|
|
|
if (failingTestDisplay.length > 0) {
|
|
report += `## Failing tests\n\n`;
|
|
report += failingTestDisplay;
|
|
report += "\n\n";
|
|
}
|
|
|
|
// if(passingTestDisplay.length > 0) {
|
|
// report += `## Passing tests\n\n`;
|
|
// report += passingTestDisplay;
|
|
// report += "\n\n";
|
|
// }
|
|
|
|
if (failing_tests.length) {
|
|
report += `## Failing tests log output\n\n`;
|
|
for (const { path, output, reason } of failing_tests) {
|
|
report += `### ${path}\n\n`;
|
|
report += "[Link to file](" + linkToGH(path) + ")\n\n";
|
|
report += `${reason}\n\n`;
|
|
report += "```\n";
|
|
|
|
let failing_output = output
|
|
.replace(/\x1b\[[0-9;]*m/g, "")
|
|
.replace(/^::(group|endgroup|error|warning|set-output|add-matcher|remove-matcher).*$/gm, "");
|
|
|
|
if (failing_output.length > 1024 * 64) {
|
|
failing_output = failing_output.slice(0, 1024 * 64) + `\n\n[truncated output (length: ${failing_output.length})]`;
|
|
}
|
|
|
|
report += failing_output;
|
|
|
|
report += "```\n\n";
|
|
}
|
|
}
|
|
|
|
writeFileSync("test-report.md", report);
|
|
writeFileSync(
|
|
"test-report.json",
|
|
JSON.stringify({
|
|
failing_tests,
|
|
passing_tests,
|
|
}),
|
|
);
|
|
|
|
function mabeCapitalize(str) {
|
|
str = str.toLowerCase();
|
|
if (str.includes("arm64") || str.includes("aarch64")) {
|
|
return str.toUpperCase();
|
|
}
|
|
|
|
if (str.includes("x64")) {
|
|
return "x64";
|
|
}
|
|
|
|
if (str.includes("baseline")) {
|
|
return str;
|
|
}
|
|
|
|
return str[0].toUpperCase() + str.slice(1);
|
|
}
|
|
|
|
console.log("-> test-report.md, test-report.json");
|
|
function linkify(text, url) {
|
|
if (url?.startsWith?.("https://")) {
|
|
return `[${text}](${url})`;
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
if (ci) {
|
|
if (failing_tests.length > 0) {
|
|
action.setFailed(`${failing_tests.length} files with failing tests`);
|
|
}
|
|
action.setOutput("failing_tests", failingTestDisplay);
|
|
action.setOutput("failing_tests_count", failing_tests.length);
|
|
if (failing_tests.length) {
|
|
const { env } = process;
|
|
const tag = process.env.BUN_TAG || "unknown";
|
|
const url = `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`;
|
|
|
|
let comment = `## ${linkify(`${emojiTag(tag)}${failing_tests.length} failing tests`, url)} ${tag
|
|
.split("-")
|
|
.map(mabeCapitalize)
|
|
.join(" ")}
|
|
|
|
${failingTestDisplay}
|
|
|
|
`;
|
|
writeFileSync("comment.md", comment);
|
|
}
|
|
let truncated_report = report;
|
|
if (truncated_report.length > 512 * 1000) {
|
|
truncated_report = truncated_report.slice(0, 512 * 1000) + "\n\n...truncated...";
|
|
}
|
|
action.summary.addRaw(truncated_report);
|
|
await action.summary.write();
|
|
}
|
|
|
|
function emojiTag(tag) {
|
|
let emojiText = "";
|
|
tag = tag.toLowerCase();
|
|
if (tag.includes("win32") || tag.includes("windows")) {
|
|
emojiText += "🪟";
|
|
}
|
|
|
|
if (tag.includes("linux")) {
|
|
emojiText += "🐧";
|
|
}
|
|
|
|
if (tag.includes("macos") || tag.includes("darwin")) {
|
|
emojiText += "";
|
|
}
|
|
|
|
if (tag.includes("x86") || tag.includes("x64") || tag.includes("_64") || tag.includes("amd64")) {
|
|
if (!tag.includes("linux")) {
|
|
emojiText += "💻";
|
|
} else {
|
|
emojiText += "🖥";
|
|
}
|
|
}
|
|
|
|
if (tag.includes("arm64") || tag.includes("aarch64")) {
|
|
emojiText += "💪";
|
|
}
|
|
|
|
if (emojiText) {
|
|
emojiText += " ";
|
|
}
|
|
|
|
return emojiText;
|
|
}
|
|
|
|
process.exit(failing_tests.length ? 1 : process.exitCode);
|