Files
bun.sh/test/harness.ts
Michael H ba20670da3 implement pnpm migration (#22262)
### What does this PR do?

fixes #7157, fixes #14662

migrates pnpm-workspace.yaml data to package.json & converts
pnpm-lock.yml to bun.lock

---

### How did you verify your code works?

manually, tests and real world examples

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
2025-09-27 00:45:29 -07:00

1890 lines
64 KiB
TypeScript

/**
* This file is loaded in every test file in the repository.
*
* Avoid adding external dependencies here so that we can still run some tests
* without always needing to run `bun install` in development.
*/
import { gc as bunGC, sleepSync, spawnSync, unsafe, which, write } from "bun";
import { heapStats } from "bun:jsc";
import { beforeAll, describe, expect } from "bun:test";
import { ChildProcess, execSync, fork } from "child_process";
import { readdir, readFile, readlink, rm, writeFile } from "fs/promises";
import fs, { closeSync, openSync, rmSync } from "node:fs";
import os from "node:os";
import { dirname, isAbsolute, join } from "path";
import * as numeric from "_util/numeric.ts";
type Awaitable<T> = T | Promise<T>;
export const BREAKING_CHANGES_BUN_1_2 = false;
export const isMacOS = process.platform === "darwin";
export const isLinux = process.platform === "linux";
export const isPosix = isMacOS || isLinux;
export const isWindows = process.platform === "win32";
export const isIntelMacOS = isMacOS && process.arch === "x64";
export const isArm64 = process.arch === "arm64";
export const isDebug = Bun.version.includes("debug");
export const isCI = process.env.CI !== undefined;
export const libcFamily: "glibc" | "musl" =
process.platform !== "linux"
? "glibc"
: // process.report.getReport() has incorrect type definitions.
(process.report.getReport() as { header: { glibcVersionRuntime: boolean } }).header.glibcVersionRuntime
? "glibc"
: "musl";
export const isMusl = isLinux && libcFamily === "musl";
export const isGlibc = isLinux && libcFamily === "glibc";
export const isBuildKite = process.env.BUILDKITE === "true";
export const isVerbose = process.env.DEBUG === "1";
// Use these to mark a test as flaky or broken.
// This will help us keep track of these tests.
//
// test.todoIf(isFlaky && isMacOS)("this test is flaky");
export const isFlaky = isCI;
export const isBroken = isCI;
export const isASAN = basename(process.execPath).includes("bun-asan");
export const bunEnv: NodeJS.Dict<string> = {
...process.env,
GITHUB_ACTIONS: "false",
BUN_DEBUG_QUIET_LOGS: "1",
NO_COLOR: "1",
FORCE_COLOR: undefined,
TZ: "Etc/UTC",
CI: "1",
BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0",
BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: "1",
BUN_GARBAGE_COLLECTOR_LEVEL: process.env.BUN_GARBAGE_COLLECTOR_LEVEL || "0",
BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE: "1",
BUN_DEBUG_linkerctx: "0",
WANTS_LOUD: "0",
};
const ciEnv = { ...bunEnv };
if (isASAN) {
bunEnv.ASAN_OPTIONS ??= "allow_user_segv_handler=1:disable_coredump=0";
}
if (isWindows) {
bunEnv.SHELLOPTS = "igncr"; // Ignore carriage return
}
for (let key in bunEnv) {
if (bunEnv[key] === undefined) {
delete ciEnv[key];
delete bunEnv[key];
}
if (key.startsWith("BUN_DEBUG_") && key !== "BUN_DEBUG_QUIET_LOGS") {
delete ciEnv[key];
delete bunEnv[key];
}
if (key.startsWith("BUILDKITE")) {
delete bunEnv[key];
delete process.env[key];
}
}
delete bunEnv.BUN_INSPECT_CONNECT_TO;
delete bunEnv.NODE_ENV;
if (isDebug) {
// This makes debug build memory leak tests more reliable.
// The code for dumping out the debug build transpiled source code has leaks.
bunEnv.BUN_DEBUG_NO_DUMP = "1";
}
export function bunExe() {
if (isWindows) return process.execPath.replaceAll("\\", "/");
return process.execPath;
}
export function nodeExe(): string | null {
return which("node") || null;
}
export function shellExe(): string {
return isWindows ? "pwsh" : "bash";
}
export function gc(force = true) {
bunGC(force);
}
/**
* The garbage collector is not 100% deterministic
*
* We want to assert that SOME of the objects are collected
* But we cannot reliably assert that ALL of them are collected
*
* Therefore, we check that the count is less than or equal to the expected count
*
* @param type
* @param count
* @param maxWait
* @returns
*/
export async function expectMaxObjectTypeCount(
expect: typeof import("bun:test").expect,
type: string,
count: number,
maxWait = 1000,
) {
var { heapStats } = require("bun:jsc");
gc();
if (heapStats().objectTypeCounts[type] <= count) return;
gc(true);
for (const wait = 20; maxWait > 0; maxWait -= wait) {
if (heapStats().objectTypeCounts[type] <= count) break;
await Bun.sleep(wait);
gc();
}
expect(heapStats().objectTypeCounts[type] || 0).toBeLessThanOrEqual(count);
}
// we must ensure that finalizers are run
// so that the reference-counting logic is exercised
export function gcTick(trace = false) {
trace && console.trace("");
// console.trace("hello");
gc();
return Bun.sleep(0);
}
export function withoutAggressiveGC(block: () => unknown) {
if (!unsafe.gcAggressionLevel) return block();
const origGC = unsafe.gcAggressionLevel();
unsafe.gcAggressionLevel(0);
try {
return block();
} finally {
unsafe.gcAggressionLevel(origGC);
}
}
export function hideFromStackTrace(block: CallableFunction) {
Object.defineProperty(block, "name", {
value: "::bunternal::",
configurable: true,
enumerable: true,
writable: true,
});
}
export type DirectoryTree = {
[name: string]:
| string
| Buffer
| DirectoryTree
| ((opts: { root: string }) => Awaitable<string | Buffer | DirectoryTree>);
};
export async function makeTree(base: string, tree: DirectoryTree) {
const isDirectoryTree = (value: string | DirectoryTree | Buffer): value is DirectoryTree =>
typeof value === "object" && value && typeof value?.byteLength === "undefined";
for (const [name, raw_contents] of Object.entries(tree)) {
const contents = typeof raw_contents === "function" ? await raw_contents({ root: base }) : raw_contents;
const joined = join(base, name);
if (name.includes("/")) {
const dir = dirname(name);
if (dir !== name && dir !== ".") {
fs.mkdirSync(join(base, dir), { recursive: true });
}
}
if (isDirectoryTree(contents)) {
fs.mkdirSync(joined);
makeTree(joined, contents);
continue;
}
fs.writeFileSync(joined, contents);
}
}
export function makeTreeSyncFromDirectoryTree(base: string, tree: DirectoryTree) {
const isDirectoryTree = (value: string | DirectoryTree | Buffer): value is DirectoryTree =>
typeof value === "object" && value && typeof value?.byteLength === "undefined";
for (const [name, raw_contents] of Object.entries(tree)) {
const contents = (typeof raw_contents === "function" ? raw_contents({ root: base }) : raw_contents) as string;
const joined = join(base, name);
if (name.includes("/")) {
const dir = dirname(name);
if (dir !== name && dir !== ".") {
fs.mkdirSync(join(base, dir), { recursive: true });
}
}
if (isDirectoryTree(contents)) {
fs.mkdirSync(joined);
makeTreeSync(joined, contents);
continue;
}
fs.writeFileSync(joined, contents);
}
}
export function makeTreeSync(base: string, filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string) {
if (typeof filesOrAbsolutePathToCopyFolderFrom === "string") {
fs.cpSync(filesOrAbsolutePathToCopyFolderFrom, base, { recursive: true });
return;
}
return makeTreeSyncFromDirectoryTree(base, filesOrAbsolutePathToCopyFolderFrom);
}
/**
* Recursively create files within a new temporary directory.
*
* @param basename prefix of the new temporary directory
* @param filesOrAbsolutePathToCopyFolderFrom Directory tree or absolute path to a folder to copy. If passing an object each key is a folder or file, and each value is the contents of the file. Use objects for directories.
* @returns an absolute path to the new temporary directory
*
* @example
* ```ts
* const dir = tempDirWithFiles("my-test", {
* "index.js": `import foo from "./src/foo";`,
* "src": {
* "foo.js": `export default "foo";`,
* },
* });
* ```
*/
export function tempDirWithFiles(
basename: string,
filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string,
): string {
const base = fs.mkdtempSync(join(fs.realpathSync.native(os.tmpdir()), basename + "_"));
makeTreeSync(base, filesOrAbsolutePathToCopyFolderFrom);
return base;
}
class DisposableString extends String {
[Symbol.dispose]() {
fs.rmSync(this + "", { recursive: true, force: true });
}
[Symbol.asyncDispose]() {
return fs.promises.rm(this + "", { recursive: true, force: true });
}
}
export function tempDir(
basename: string,
filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string,
): string & DisposableString & AsyncDisposable {
const base = tempDirWithFiles(basename, filesOrAbsolutePathToCopyFolderFrom);
return new DisposableString(base) as string & DisposableString & AsyncDisposable;
}
export function tempDirWithFilesAnon(filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string): string {
const base = tmpdirSync();
makeTreeSync(base, filesOrAbsolutePathToCopyFolderFrom);
return base;
}
export function bunRun(file: string, env?: Record<string, string> | NodeJS.ProcessEnv, dump = false) {
var path = require("path");
const result = Bun.spawnSync([bunExe(), file], {
cwd: path.dirname(file),
env: {
...bunEnv,
NODE_ENV: undefined,
...env,
},
stdin: "ignore",
stdout: !dump ? "pipe" : "inherit",
stderr: !dump ? "pipe" : "inherit",
});
if (!result.success) {
if (dump) {
throw new Error(
"exited with code " + result.exitCode + (result.signalCode ? `signal: ${result.signalCode}` : ""),
);
}
throw new Error(String(result.stderr) + "\n" + String(result.stdout));
}
return {
stdout: String(result.stdout ?? "").trim(),
stderr: String(result.stderr ?? "").trim(),
};
}
export function bunTest(file: string, env?: Record<string, string>) {
var path = require("path");
const result = Bun.spawnSync([bunExe(), "test", path.basename(file)], {
cwd: path.dirname(file),
env: {
...bunEnv,
NODE_ENV: undefined,
...env,
},
});
if (!result.success) throw new Error(result.stderr.toString("utf8"));
return {
stdout: result.stdout.toString("utf8").trim(),
stderr: result.stderr.toString("utf8").trim(),
};
}
export function bunRunAsScript(
dir: string,
script: string,
env?: Record<string, string | undefined>,
execArgv?: string[],
) {
const result = Bun.spawnSync([bunExe(), ...(execArgv ?? []), `run`, `${script}`], {
cwd: dir,
env: {
...bunEnv,
NODE_ENV: undefined,
...env,
},
});
if (!result.success) throw new Error(result.stderr.toString("utf8"));
return {
stdout: result.stdout.toString("utf8").trim(),
stderr: result.stderr.toString("utf8").trim(),
};
}
export function randomLoneSurrogate() {
const n = numeric.random.between(0, 2, { domain: "integral" });
if (n === 0) return randomLoneHighSurrogate();
return randomLoneLowSurrogate();
}
export function randomInvalidSurrogatePair() {
const low = randomLoneLowSurrogate();
const high = randomLoneHighSurrogate();
return `${low}${high}`;
}
// Generates a random lone high surrogate (from the range D800-DBFF)
export function randomLoneHighSurrogate() {
return String.fromCharCode(numeric.random.between(0xd800, 0xdbff, { domain: "integral" }));
}
// Generates a random lone high surrogate (from the range DC00-DFFF)
export function randomLoneLowSurrogate() {
return String.fromCharCode(numeric.random.between(0xdc00, 0xdfff, { domain: "integral" }));
}
export function runWithError(cb: () => unknown): Error | undefined {
try {
cb();
} catch (e) {
return e as Error;
}
return undefined;
}
export async function runWithErrorPromise(cb: () => unknown): Promise<Error | undefined> {
try {
await cb();
} catch (e) {
return e as Error;
}
return undefined;
}
export function fakeNodeRun(dir: string, file: string | string[], env?: Record<string, string>) {
var path = require("path");
const result = Bun.spawnSync([bunExe(), "--bun", "node", ...(Array.isArray(file) ? file : [file])], {
cwd: dir ?? path.dirname(file),
env: {
...bunEnv,
NODE_ENV: undefined,
...env,
},
});
if (!result.success) throw new Error(result.stderr.toString("utf8"));
return {
stdout: result.stdout.toString("utf8").trim(),
stderr: result.stderr.toString("utf8").trim(),
};
}
export function randomPort(): number {
return 1024 + Math.floor(Math.random() * (65535 - 1024));
}
const binaryTypes = {
"buffer": Buffer,
"arraybuffer": ArrayBuffer,
"uint8array": Uint8Array,
"uint16array": Uint16Array,
"uint32array": Uint32Array,
"int8array": Int8Array,
"int16array": Int16Array,
"int32array": Int32Array,
"float16array": globalThis.Float16Array,
"float32array": Float32Array,
"float64array": Float64Array,
} as const;
if (expect.extend)
expect.extend({
toHaveTestTimedOutAfter(actual: any, expected: number) {
if (typeof actual !== "string") {
return {
pass: false,
message: () => `Expected ${actual} to be a string`,
};
}
const preStartI = actual.indexOf("timed out after ");
if (preStartI === -1) {
return {
pass: false,
message: () => `Expected ${actual} to contain "timed out after "`,
};
}
const startI = preStartI + "timed out after ".length;
const endI = actual.indexOf("ms", startI);
if (endI === -1) {
return {
pass: false,
message: () => `Expected ${actual} to contain "ms" after "timed out after "`,
};
}
const int = parseInt(actual.slice(startI, endI));
if (!Number.isSafeInteger(int)) {
return {
pass: false,
message: () => `Expected ${int} to be a safe integer`,
};
}
return {
pass: int >= expected,
message: () => `Expected ${int} to be >= ${expected}`,
};
},
toBeBinaryType(actual: any, expected: keyof typeof binaryTypes) {
switch (expected) {
case "buffer":
return {
pass: Buffer.isBuffer(actual),
message: () => `Expected ${actual} to be buffer`,
};
case "arraybuffer":
return {
pass: actual instanceof ArrayBuffer,
message: () => `Expected ${actual} to be ArrayBuffer`,
};
default: {
const ctor = binaryTypes[expected];
if (!ctor) {
return {
pass: false,
message: () => `Expected ${expected} to be a binary type`,
};
}
return {
pass: actual instanceof ctor,
message: () => `Expected ${actual} to be ${expected}`,
};
}
}
},
toRun(cmds: string[], optionalStdout?: string, expectedCode: number = 0) {
const result = Bun.spawnSync({
cmd: [bunExe(), ...cmds],
env: bunEnv,
stdio: ["inherit", "pipe", "inherit"],
});
if (result.exitCode !== expectedCode) {
return {
pass: false,
message: () => `Command ${cmds.join(" ")} failed:` + "\n" + result.stdout.toString("utf-8"),
};
}
if (optionalStdout != null) {
return {
pass: result.stdout.toString("utf-8") === optionalStdout,
message: () =>
`Expected ${cmds.join(" ")} to output ${optionalStdout} but got ${result.stdout.toString("utf-8")}`,
};
}
return {
pass: true,
message: () => `Expected ${cmds.join(" ")} to fail`,
};
},
toThrowWithCode(fn: CallableFunction, cls: CallableFunction, code: string) {
try {
fn();
return {
pass: false,
message: () => `Received function did not throw`,
};
} catch (e) {
// expect(e).toBeInstanceOf(cls);
if (!(e instanceof cls)) {
return {
pass: false,
message: () => `Expected error to be instanceof ${cls.name}; got ${e.__proto__.constructor.name}`,
};
}
// expect(e).toHaveProperty("code");
if (!("code" in e)) {
return {
pass: false,
message: () => `Expected error to have property 'code'; got ${e}`,
};
}
// expect(e.code).toEqual(code);
if (e.code !== code) {
return {
pass: false,
message: () => `Expected error to have code '${code}'; got ${e.code}`,
};
}
return {
pass: true,
};
}
},
async toThrowWithCodeAsync(fn: CallableFunction, cls: CallableFunction, code: string) {
try {
await fn();
return {
pass: false,
message: () => `Received function did not throw`,
};
} catch (e) {
// expect(e).toBeInstanceOf(cls);
if (!(e instanceof cls)) {
return {
pass: false,
message: () => `Expected error to be instanceof ${cls.name}; got ${e.__proto__.constructor.name}`,
};
}
// expect(e).toHaveProperty("code");
if (!("code" in e)) {
return {
pass: false,
message: () => `Expected error to have property 'code'; got ${e}`,
};
}
// expect(e.code).toEqual(code);
if (e.code !== code) {
return {
pass: false,
message: () => `Expected error to have code '${code}'; got ${e.code}`,
};
}
return {
pass: true,
};
}
},
toBeLatin1String(actual: unknown) {
if ((actual as string).isLatin1()) {
return {
pass: true,
message: () => `Expected ${actual} to be a Latin1 string`,
};
}
return {
pass: false,
message: () => `Expected ${actual} to be a Latin1 string`,
};
},
toBeUTF16String(actual: unknown) {
if ((actual as string).isUTF16()) {
return {
pass: true,
message: () => `Expected ${actual} to be a UTF16 string`,
};
}
return {
pass: false,
message: () => `Expected ${actual} to be a UTF16 string`,
};
},
});
export function ospath(path: string) {
if (isWindows) {
return path.replace(/\//g, "\\");
}
return path;
}
/**
* Iterates through each tree in the lockfile, checking for each package
* on disk. Also requires each package dependency. Not tested well for
* non-npm packages (links, folders, git dependencies, etc.)
*/
export async function toMatchNodeModulesAt(lockfile: any, root: string) {
function shouldSkip(pkg: any, dep: any): boolean {
// Band-aid as toMatchNodeModulesAt will sometimes ask this function
// if a package depends on itself
if (pkg?.name === dep?.name) return true;
return (
!pkg ||
!pkg.resolution ||
dep.behavior.optional ||
(dep.behavior.dev && pkg.id !== 0) ||
(pkg.arch && pkg.arch !== process.arch)
);
}
for (const { path, dependencies } of lockfile.trees) {
for (const { package_id, id } of Object.values(dependencies) as any[]) {
const treeDep = lockfile.dependencies[id];
const treePkg = lockfile.packages[package_id];
if (shouldSkip(treePkg, treeDep)) continue;
const treeDepPath = join(root, path, treeDep.name);
switch (treePkg.resolution.tag) {
case "npm":
const onDisk = await Bun.file(join(treeDepPath, "package.json")).json();
if (!Bun.deepMatch({ name: treePkg.name, version: treePkg.resolution.value }, onDisk)) {
return {
pass: false,
message: () => `
Expected at ${join(path, treeDep.name)}: ${JSON.stringify({ name: treePkg.name, version: treePkg.resolution.value })}
Received ${JSON.stringify({ name: onDisk.name, version: onDisk.version })}`,
};
}
// Ok, we've confirmed the package exists and has the correct version. Now go through
// each of its transitive dependencies and confirm the same.
for (const depId of treePkg.dependencies) {
const dep = lockfile.dependencies[depId];
const pkg = lockfile.packages[dep.package_id];
if (shouldSkip(pkg, dep)) continue;
try {
const resolved = await Bun.file(Bun.resolveSync(join(dep.name, "package.json"), treeDepPath)).json();
switch (pkg.resolution.tag) {
case "npm":
const name = dep.is_alias ? dep.npm.name : dep.name;
if (!Bun.deepMatch({ name, version: pkg.resolution.value }, resolved)) {
if (dep.literal === "*") {
// allow any version, just needs to be resolvable
continue;
}
if (dep.behavior.peer && dep.npm) {
// allow peer dependencies to not match exactly, but still satisfy
if (Bun.semver.satisfies(pkg.resolution.value, dep.npm.version)) continue;
}
return {
pass: false,
message: () =>
`Expected ${dep.name} to have version ${pkg.resolution.value} in ${treeDepPath}, but got ${resolved.version}`,
};
}
break;
}
} catch (e) {
return {
pass: false,
message: () => `Expected ${dep.name} to be resolvable in ${treeDepPath}`,
};
}
}
break;
default:
if (!fs.existsSync(treeDepPath)) {
return {
pass: false,
message: () => `Expected ${treePkg.resolution.tag} "${treeDepPath}" to exist`,
};
}
for (const depId of treePkg.dependencies) {
const dep = lockfile.dependencies[depId];
const pkg = lockfile.packages[dep.package_id];
if (shouldSkip(pkg, dep)) continue;
try {
const resolved = await Bun.file(Bun.resolveSync(join(dep.name, "package.json"), treeDepPath)).json();
switch (pkg.resolution.tag) {
case "npm":
const name = dep.is_alias ? dep.npm.name : dep.name;
if (!Bun.deepMatch({ name, version: pkg.resolution.value }, resolved)) {
if (dep.literal === "*") {
// allow any version, just needs to be resolvable
continue;
}
// workspaces don't need a version
if (treePkg.resolution.tag === "workspace" && !resolved.version) continue;
if (dep.behavior.peer && dep.npm) {
// allow peer dependencies to not match exactly, but still satisfy
if (Bun.semver.satisfies(pkg.resolution.value, dep.npm.version)) continue;
}
return {
pass: false,
message: () =>
`Expected ${dep.name} to have version ${pkg.resolution.value} in ${treeDepPath}, but got ${resolved.version}`,
};
}
break;
}
} catch (e) {
return {
pass: false,
message: () => `Expected ${dep.name} to be resolvable in ${treeDepPath}`,
};
}
}
break;
}
}
}
return {
pass: true,
};
}
export async function toHaveBins(actual: string[], expectedBins: string[]) {
const message = () => `Expected ${actual} to be package bins ${expectedBins}`;
if (isWindows) {
for (var i = 0; i < actual.length; i += 2) {
if (!actual[i].includes(expectedBins[i / 2]) || !actual[i + 1].includes(expectedBins[i / 2])) {
return { pass: false, message };
}
}
return { pass: true, message };
}
return { pass: actual.every((bin, i) => bin === expectedBins[i]), message };
}
export async function toBeValidBin(actual: string, expectedLinkPath: string) {
const message = () => `Expected ${actual} to be a link to ${expectedLinkPath}`;
if (isWindows) {
const contents = await readFile(actual + ".bunx", "utf16le");
const expected = expectedLinkPath.slice(3);
return { pass: contents.includes(expected), message };
}
return { pass: (await readlink(actual)) === expectedLinkPath, message };
}
export async function toBeWorkspaceLink(actual: string, expectedLinkPath: string) {
const message = () => `Expected ${actual} to be a link to ${expectedLinkPath}`;
if (isWindows) {
// junctions on windows will have an absolute path
const pass = isAbsolute(actual) && actual.includes(expectedLinkPath.split("..").at(-1)!);
return { pass, message };
}
const pass = actual === expectedLinkPath;
return { pass, message };
}
export function getMaxFD(): number {
if (isMacOS || isLinux) {
let max = -1;
// https://github.com/python/cpython/commit/e21a7a976a7e3368dc1eba0895e15c47cb06c810
for (let entry of fs.readdirSync(isMacOS ? "/dev/fd" : "/proc/self/fd")) {
const fd = parseInt(entry.trim(), 10);
if (Number.isSafeInteger(fd) && fd >= 0) {
max = Math.max(max, fd);
}
}
if (max >= 0) {
return max;
}
}
const maxFD = openSync("/dev/null", "r");
closeSync(maxFD);
return maxFD;
}
// This is extremely frowned upon but I think it's easier to deal with than
// remembering to do this manually everywhere
declare global {
interface Buffer {
/**
* **INTERNAL USE ONLY, NOT An API IN BUN**
*/
toUnixString(): string;
}
interface String {
/**
* **INTERNAL USE ONLY, NOT An API IN BUN**
*/
isLatin1(): boolean;
/**
* **INTERNAL USE ONLY, NOT An API IN BUN**
*/
isUTF16(): boolean;
}
}
Buffer.prototype.toUnixString = function () {
return this.toString("utf-8").replaceAll("\r\n", "\n");
};
export function dockerExe(): string | null {
return which("docker") || which("podman") || null;
}
export function isDockerEnabled(): boolean {
const dockerCLI = dockerExe();
if (!dockerCLI) {
if (isCI && isLinux) {
throw new Error("A functional `docker` is required in CI for some tests.");
}
return false;
}
// TODO: investigate why Docker tests are not working on Linux arm64
if (isLinux && process.arch === "arm64") {
return false;
}
try {
const info = execSync(`"${dockerCLI}" info`, { stdio: ["ignore", "pipe", "inherit"] });
return info.toString().indexOf("Server Version:") !== -1;
} catch {
if (isCI && isLinux) {
throw new Error("A functional `docker` is required in CI for some tests.");
}
return false;
}
}
export async function waitForPort(port: number, timeout: number = 60_000): Promise<void> {
let deadline = Date.now() + Math.max(1, timeout);
let error: unknown;
while (Date.now() < deadline) {
error = await new Promise(resolve => {
Bun.connect({
hostname: "localhost",
port,
socket: {
data: socket => {
resolve(undefined);
socket.end();
},
end: () => resolve(new Error("Socket closed")),
error: (_, cause) => resolve(new Error("Socket error", { cause })),
connectError: (_, cause) => resolve(new Error("Socket connect error", { cause })),
},
});
});
if (error) {
await Bun.sleep(1000);
} else {
return;
}
}
throw error;
}
export async function describeWithContainer(
label: string,
{
image,
env = {},
args = [],
archs,
concurrent = false,
}: {
image: string;
env?: Record<string, string>;
args?: string[];
archs?: NodeJS.Architecture[];
concurrent?: boolean;
},
fn: (container: { port: number; host: string; ready: Promise<void> }) => void,
) {
// Skip if Docker is not available
if (!isDockerEnabled()) {
describe.todo(label);
return;
}
(concurrent && Bun.version !== "1.2.22" ? describe.concurrent : describe)(label, () => {
// Check if this is one of our docker-compose services
const services: Record<string, number> = {
"postgres_plain": 5432,
"postgres_tls": 5432,
"postgres_auth": 5432,
"mysql_plain": 3306,
"mysql_native_password": 3306,
"mysql_tls": 3306,
"mysql:8": 3306, // Map mysql:8 to mysql_plain
"mysql:9": 3306, // Map mysql:9 to mysql_native_password
"redis_plain": 6379,
"redis_unified": 6379,
"minio": 9000,
"autobahn": 9002,
};
const servicePort = services[image];
if (servicePort) {
// Map mysql:8 and mysql:9 based on environment variables
let actualService = image;
if (image === "mysql:8" || image === "mysql:9") {
if (env.MYSQL_ROOT_PASSWORD === "bun") {
actualService = "mysql_native_password"; // Has password "bun"
} else if (env.MYSQL_ALLOW_EMPTY_PASSWORD === "yes") {
actualService = "mysql_plain"; // No password
} else {
actualService = "mysql_plain"; // Default to no password
}
}
// Create a container descriptor with stable references and a ready promise
let readyResolver: () => void;
let readyRejecter: (error: any) => void;
const readyPromise = new Promise<void>((resolve, reject) => {
readyResolver = resolve;
readyRejecter = reject;
});
// Internal state that will be updated when container is ready
let _host = "127.0.0.1";
let _port = 0;
// Container descriptor with live getters and ready promise
const containerDescriptor = {
get host() {
return _host;
},
get port() {
return _port;
},
ready: readyPromise,
};
// Start the service before any tests
beforeAll(async () => {
try {
const dockerHelper = await import("./docker/index.ts");
const info = await dockerHelper.ensure(actualService as any);
_host = info.host;
_port = info.ports[servicePort];
console.log(`Container ready via docker-compose: ${image} at ${_host}:${_port}`);
readyResolver!();
} catch (error) {
readyRejecter!(error);
throw error;
}
});
fn(containerDescriptor);
return;
}
// No fallback - if the image isn't in docker-compose, it should fail
throw new Error(
`Image "${image}" is not configured in docker-compose.yml. All test containers must use docker-compose.`,
);
});
}
export function osSlashes(path: string) {
return isWindows ? path.replace(/\//g, "\\") : path;
}
import * as child_process from "node:child_process";
import { basename } from "node:path";
class WriteBlockedError extends Error {
constructor(time) {
super("Write blocked for " + (time | 0) + "ms");
this.name = "WriteBlockedError";
}
}
function failTestsOnBlockingWriteCall() {
const prop = Object.getOwnPropertyDescriptor(child_process.ChildProcess.prototype, "stdin");
const didAttachSymbol = Symbol("kDidAttach");
if (prop) {
Object.defineProperty(child_process.ChildProcess.prototype, "stdin", {
...prop,
get() {
const actual = prop.get.call(this);
if (actual?.write && !actual.__proto__[didAttachSymbol]) {
actual.__proto__[didAttachSymbol] = true;
attachWriteMeasurement(actual);
}
return actual;
},
});
}
function attachWriteMeasurement(stream) {
const prop = Object.getOwnPropertyDescriptor(stream.__proto__, "write");
if (prop) {
Object.defineProperty(stream.__proto__, "write", {
...prop,
value(chunk, encoding, cb) {
const start = performance.now();
const rc = prop.value.apply(this, arguments);
const end = performance.now();
if (end - start > 8) {
const err = new WriteBlockedError(end - start);
throw err;
}
return rc;
},
});
}
}
}
failTestsOnBlockingWriteCall();
export function dumpStats() {
const stats = heapStats();
const { objectTypeCounts, protectedObjectTypeCounts } = stats;
console.log({
objects: Object.fromEntries(Object.entries(objectTypeCounts).sort()),
protected: Object.fromEntries(Object.entries(protectedObjectTypeCounts).sort()),
});
}
export function fillRepeating(dstBuffer: NodeJS.TypedArray, start: number, end: number) {
let len = dstBuffer.length, // important: use indices length, not byte-length
sLen = end - start,
p = sLen; // set initial position = source sequence length
// step 2: copy existing data doubling segment length per iteration
while (p < len) {
if (p + sLen > len) sLen = len - p; // if not power of 2, truncate last segment
dstBuffer.copyWithin(p, start, sLen); // internal copy
p += sLen; // add current length to offset
sLen <<= 1; // double length for next segment
}
}
function makeFlatPropertyMap(opts: object) {
// return all properties of opts as paths for nested objects with dot notation
// like { a: { b: 1 } } => { "a.b": 1 }
// combining names of nested objects with dot notation
// infinitely deep
const ret: any = {};
function recurse(obj: object, path = "") {
for (const [key, value] of Object.entries(obj)) {
if (value === undefined) continue;
if (value && typeof value === "object") {
recurse(value, path ? `${path}.${key}` : key);
} else {
ret[path ? `${path}.${key}` : key] = value;
}
}
}
recurse(opts);
return ret;
}
export function toTOMLString(opts: object) {
// return a TOML string of the given options
const props = makeFlatPropertyMap(opts);
let ret = "";
for (const [key, value] of Object.entries(props)) {
if (value === undefined) continue;
ret += `${key} = ${JSON.stringify(value)}` + "\n";
}
return ret;
}
const shebang_posix = (program: string) => `#!/usr/bin/env ${program}
`;
const shebang_windows = (program: string) => `0</* :{
@echo off
${program} %~f0 %*
exit /b %errorlevel%
:} */0;
`;
export function writeShebangScript(path: string, program: string, data: string) {
if (!isWindows) {
return writeFile(path, shebang_posix(program) + "\n" + data, { mode: 0o777 });
} else {
return writeFile(path + ".cmd", shebang_windows(program) + "\n" + data);
}
}
export async function* forEachLine(iter: AsyncIterable<NodeJS.TypedArray | ArrayBufferLike>) {
var decoder = new (require("string_decoder").StringDecoder)("utf8");
var str = "";
for await (const chunk of iter) {
str += decoder.write(chunk);
let i = str.indexOf("\n");
while (i >= 0) {
yield str.slice(0, i);
str = str.slice(i + 1);
i = str.indexOf("\n");
}
}
str += decoder.end();
{
let i = str.indexOf("\n");
while (i >= 0) {
yield str.slice(0, i);
str = str.slice(i + 1);
i = str.indexOf("\n");
}
}
if (str.length > 0) {
yield str;
}
}
export function joinP(...paths: string[]) {
return join(...paths).replaceAll("\\", "/");
}
/**
* TODO: see if this is the default behavior of node child_process APIs if so,
* we need to do case-insensitive stuff within our Bun.spawn implementation
*
* Windows has case-insensitive environment variables, so sometimes an
* object like { Path: "...", PATH: "..." } will be passed. Bun lets
* the first one win, but we really want the LAST one to win.
*
* This is mostly needed if you want to override env vars, such like:
* env: {
* ...bunEnv,
* PATH: "my path override here",
* }
* becomes
* env: mergeWindowEnvs([
* bunEnv,
* {
* PATH: "my path override here",
* },
* ])
*/
export function mergeWindowEnvs(envs: Record<string, string | undefined>[]) {
const keys: Record<string, string | undefined> = {};
const flat: Record<string, string | undefined> = {};
for (const env of envs) {
for (const key in env) {
if (!env[key]) continue;
const normalized = (keys[key.toUpperCase()] ??= key);
flat[normalized] = env[key];
}
}
return flat;
}
export function tmpdirSync(pattern: string = "bun.test."): string {
return fs.mkdtempSync(join(fs.realpathSync.native(os.tmpdir()), pattern));
}
export async function runBunInstall(
env: NodeJS.Dict<string>,
cwd: string,
options: {
allowWarnings?: boolean;
allowErrors?: boolean;
expectedExitCode?: number | null;
savesLockfile?: boolean;
production?: boolean;
frozenLockfile?: boolean;
saveTextLockfile?: boolean;
packages?: string[];
verbose?: boolean;
} = {},
) {
const production = options?.production ?? false;
const args = [bunExe(), "install"];
if (options?.packages) {
args.push(...options.packages);
}
if (production) {
args.push("--production");
}
if (options?.frozenLockfile) {
args.push("--frozen-lockfile");
}
if (options?.saveTextLockfile) {
args.push("--save-text-lockfile");
}
if (options?.verbose) {
args.push("--verbose");
}
const { stdout, stderr, exited } = Bun.spawn({
cmd: args,
cwd,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
expect(stdout).toBeDefined();
expect(stderr).toBeDefined();
let err: string = stderrForInstall(await stderr.text());
expect(err).not.toContain("panic:");
if (!options?.allowErrors) {
expect(err).not.toContain("error:");
}
if (!options?.allowWarnings) {
expect(err).not.toContain("warn:");
}
if ((options?.savesLockfile ?? true) && !production && !options?.frozenLockfile) {
expect(err).toContain("Saved lockfile");
}
let out: string = await stdout.text();
expect(await exited).toBe(options?.expectedExitCode ?? 0);
return { out, err, exited };
}
// stderr with `slow filesystem` warning removed
export function stderrForInstall(err: string) {
return err.replace(/warn: Slow filesystem.*/g, "");
}
export async function runBunUpdate(
env: NodeJS.ProcessEnv,
cwd: string,
args?: string[],
): Promise<{ out: string[]; err: string; exitCode: number }> {
const { stdout, stderr, exited } = Bun.spawn({
cmd: [bunExe(), "update", ...(args ?? [])],
cwd,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
let err = await stderr.text();
let out = await stdout.text();
let exitCode = await exited;
if (exitCode !== 0) {
console.log("stdout:", out);
console.log("stderr:", err);
expect().fail("bun update failed");
}
return { out: out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/), err, exitCode };
}
export async function pack(cwd: string, env: NodeJS.Dict<string>, ...args: string[]) {
const { stdout, stderr, exited } = Bun.spawn({
cmd: [bunExe(), "pm", "pack", ...args],
cwd,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).not.toContain("warning:");
expect(err).not.toContain("failed");
expect(err).not.toContain("panic:");
const out = await stdout.text();
const exitCode = await exited;
expect(exitCode).toBe(0);
return { out, err };
}
// If you need to modify, clone it
export const expiredTls = Object.freeze({
cert: "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAKLdQVPy90jjMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\nBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\naWRnaXRzIFB0eSBMdGQwHhcNMTkwMjAzMTQ0OTM1WhcNMjAwMjAzMTQ0OTM1WjBF\nMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\nZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA7i7IIEdICTiSTVx+ma6xHxOtcbd6wGW3nkxlCkJ1UuV8NmY5ovMsGnGD\nhJJtUQ2j5ig5BcJUf3tezqCNW4tKnSOgSISfEAKvpn2BPvaFq3yx2Yjz0ruvcGKp\nDMZBXmB/AAtGyN/UFXzkrcfppmLHJTaBYGG6KnmU43gPkSDy4iw46CJFUOupc51A\nFIz7RsE7mbT1plCM8e75gfqaZSn2k+Wmy+8n1HGyYHhVISRVvPqkS7gVLSVEdTea\nUtKP1Vx/818/HDWk3oIvDVWI9CFH73elNxBkMH5zArSNIBTehdnehyAevjY4RaC/\nkK8rslO3e4EtJ9SnA4swOjCiqAIQEwIDAQABo1AwTjAdBgNVHQ4EFgQUv5rc9Smm\n9c4YnNf3hR49t4rH4yswHwYDVR0jBBgwFoAUv5rc9Smm9c4YnNf3hR49t4rH4ysw\nDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATcL9CAAXg0u//eYUAlQa\nL+l8yKHS1rsq1sdmx7pvsmfZ2g8ONQGfSF3TkzkI2OOnCBokeqAYuyT8awfdNUtE\nEHOihv4ZzhK2YZVuy0fHX2d4cCFeQpdxno7aN6B37qtsLIRZxkD8PU60Dfu9ea5F\nDDynnD0TUabna6a0iGn77yD8GPhjaJMOz3gMYjQFqsKL252isDVHEDbpVxIzxPmN\nw1+WK8zRNdunAcHikeoKCuAPvlZ83gDQHp07dYdbuZvHwGj0nfxBLc9qt90XsBtC\n4IYR7c/bcLMmKXYf0qoQ4OzngsnPI5M+v9QEHvYWaKVwFY4CTcSNJEwfXw+BAeO5\nOA==\n-----END CERTIFICATE-----",
key: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDuLsggR0gJOJJN\nXH6ZrrEfE61xt3rAZbeeTGUKQnVS5Xw2Zjmi8ywacYOEkm1RDaPmKDkFwlR/e17O\noI1bi0qdI6BIhJ8QAq+mfYE+9oWrfLHZiPPSu69wYqkMxkFeYH8AC0bI39QVfOSt\nx+mmYsclNoFgYboqeZTjeA+RIPLiLDjoIkVQ66lznUAUjPtGwTuZtPWmUIzx7vmB\n+pplKfaT5abL7yfUcbJgeFUhJFW8+qRLuBUtJUR1N5pS0o/VXH/zXz8cNaTegi8N\nVYj0IUfvd6U3EGQwfnMCtI0gFN6F2d6HIB6+NjhFoL+QryuyU7d7gS0n1KcDizA6\nMKKoAhATAgMBAAECggEAd5g/3o1MK20fcP7PhsVDpHIR9faGCVNJto9vcI5cMMqP\n6xS7PgnSDFkRC6EmiLtLn8Z0k2K3YOeGfEP7lorDZVG9KoyE/doLbpK4MfBAwBG1\nj6AHpbmd5tVzQrnNmuDjBBelbDmPWVbD0EqAFI6mphXPMqD/hFJWIz1mu52Kt2s6\n++MkdqLO0ORDNhKmzu6SADQEcJ9Suhcmv8nccMmwCsIQAUrfg3qOyqU4//8QB8ZM\njosO3gMUesihVeuF5XpptFjrAliPgw9uIG0aQkhVbf/17qy0XRi8dkqXj3efxEDp\n1LSqZjBFiqJlFchbz19clwavMF/FhxHpKIhhmkkRSQKBgQD9blaWSg/2AGNhRfpX\nYq+6yKUkUD4jL7pmX1BVca6dXqILWtHl2afWeUorgv2QaK1/MJDH9Gz9Gu58hJb3\nymdeAISwPyHp8euyLIfiXSAi+ibKXkxkl1KQSweBM2oucnLsNne6Iv6QmXPpXtro\nnTMoGQDS7HVRy1on5NQLMPbUBQKBgQDwmN+um8F3CW6ZV1ZljJm7BFAgNyJ7m/5Q\nYUcOO5rFbNsHexStrx/h8jYnpdpIVlxACjh1xIyJ3lOCSAWfBWCS6KpgeO1Y484k\nEYhGjoUsKNQia8UWVt+uWnwjVSDhQjy5/pSH9xyFrUfDg8JnSlhsy0oC0C/PBjxn\nhxmADSLnNwKBgQD2A51USVMTKC9Q50BsgeU6+bmt9aNMPvHAnPf76d5q78l4IlKt\nwMs33QgOExuYirUZSgjRwknmrbUi9QckRbxwOSqVeMOwOWLm1GmYaXRf39u2CTI5\nV9gTMHJ5jnKd4gYDnaA99eiOcBhgS+9PbgKSAyuUlWwR2ciL/4uDzaVeDQKBgDym\nvRSeTRn99bSQMMZuuD5N6wkD/RxeCbEnpKrw2aZVN63eGCtkj0v9LCu4gptjseOu\n7+a4Qplqw3B/SXN5/otqPbEOKv8Shl/PT6RBv06PiFKZClkEU2T3iH27sws2EGru\nw3C3GaiVMxcVewdg1YOvh5vH8ZVlxApxIzuFlDvnAoGAN5w+gukxd5QnP/7hcLDZ\nF+vesAykJX71AuqFXB4Wh/qFY92CSm7ImexWA/L9z461+NKeJwb64Nc53z59oA10\n/3o2OcIe44kddZXQVP6KTZBd7ySVhbtOiK3/pCy+BQRsrC7d71W914DxNWadwZ+a\njtwwKjDzmPwdIXDSQarCx0U=\n-----END PRIVATE KEY-----",
passphrase: "1234",
});
// openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
// -keyout localhost.key \
// -out localhost.crt \
// -subj "/C=US/ST=CA/L=San Francisco/O=Oven/OU=Team Bun/CN=server-bun" \
// -addext "subjectAltName = DNS:localhost,IP:127.0.0.1,IP:::1"
// notAfter=Sep 4 03:00:49 2035 GMT
export const tls = Object.freeze({
cert: "-----BEGIN CERTIFICATE-----\nMIID4jCCAsqgAwIBAgIUcaRq6J/YF++Bo01Zc+HeQvCbnWMwDQYJKoZIhvcNAQEL\nBQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh\nbmNpc2NvMQ0wCwYDVQQKDARPdmVuMREwDwYDVQQLDAhUZWFtIEJ1bjETMBEGA1UE\nAwwKc2VydmVyLWJ1bjAeFw0yNTA5MDYwMzAwNDlaFw0zNTA5MDQwMzAwNDlaMGkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNj\nbzENMAsGA1UECgwET3ZlbjERMA8GA1UECwwIVGVhbSBCdW4xEzARBgNVBAMMCnNl\ncnZlci1idW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlYzosgRgX\nHL6vMh1V0ERFhsvlZrtRojSw6tafr3SQBphU793/rGiYZlL/lJ9HIlLkx9JMbuTj\nNm5U2eRwHiTQIeWD4aCIESwPlkdaVYtC+IOj55bJN8xNa7h5GyJwF7PnPetAsKyE\n8DMBn1gKMhaIis7HHOUtk4/K3Y4peU44d04z0yPt6JtY5Sbvi1E7pGX6T/2c9sHs\ndIDeDctWnewpXXs8zkAla0KNWQfpDnpS53wxAfStTA4lSrA9daxC7hZopQlLxFIb\nJk+0BLbEsXtrJ54T5iguHk+2MDVAy4MOqP9XbKV7eGHk73l6+CSwmHyHBxh4ChxR\nQeT5BP0MUTn1AgMBAAGjgYEwfzAdBgNVHQ4EFgQUw7nEnh4uOdZVZUapQzdAUaVa\nAn0wHwYDVR0jBBgwFoAUw7nEnh4uOdZVZUapQzdAUaVaAn0wDwYDVR0TAQH/BAUw\nAwEB/zAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAA\nAAEwDQYJKoZIhvcNAQELBQADggEBAEA8r1fvDLMSCb8bkAURpFk8chn8pl5MChzT\nYUDaLdCCBjPXJkSXNdyuwS+T/ljAGyZbW5xuDccCNKltawO4CbyEXUEZbYr3w9eq\nj8uqymJPhFf0O1rKOI2han5GBCgHwG13QwKI+4uu7390nD+TlzLOhxFfvOG7OadH\nQNMNLNyldgF4Nb8vWdz0FtQiGUIrO7iq4LFhhd1lCxe0q+FAYSEYcc74WtF/Yo8V\nJQauXuXyoP5FqLzNt/yeNQhceyIXJGKCsjr5/bASBmVlCwgRfsD3jpG37L8YCJs1\nL4WEikcY4Lzb2NF9e94IyZdQsRqd9DFBF5zP013MSUiuhiow32k=\n-----END CERTIFICATE-----\n",
key: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDlYzosgRgXHL6v\nMh1V0ERFhsvlZrtRojSw6tafr3SQBphU793/rGiYZlL/lJ9HIlLkx9JMbuTjNm5U\n2eRwHiTQIeWD4aCIESwPlkdaVYtC+IOj55bJN8xNa7h5GyJwF7PnPetAsKyE8DMB\nn1gKMhaIis7HHOUtk4/K3Y4peU44d04z0yPt6JtY5Sbvi1E7pGX6T/2c9sHsdIDe\nDctWnewpXXs8zkAla0KNWQfpDnpS53wxAfStTA4lSrA9daxC7hZopQlLxFIbJk+0\nBLbEsXtrJ54T5iguHk+2MDVAy4MOqP9XbKV7eGHk73l6+CSwmHyHBxh4ChxRQeT5\nBP0MUTn1AgMBAAECggEABtPvC5uVGr0DjQX2GxONsK8cOxoVec7U+C4pUMwBcXcM\nyjxwlHdujpi/IDXtjsm+A2rSPu2vGPdKDfMFanPvPxW/Ne99noc6U0VzHsR8lnP8\nwSB328nyJhzOeyZcXk9KTtgIPF7156gZsJLsZTNL+ej90i3xQWvKxCxXmrLuad5O\nz/TrgZkC6wC3fgj1d3e8bMljQ7tLxbshJMYVI5o6RFTxy84DLI+rlvPkf7XbiMPf\n2lsm4jcJKvfx+164HZJ9QVlx8ncqOHAnGvxb2xHHfqv4JAbz615t7yRvtaw4Paj5\n6kQSf0VWnsVzgxNJWvnUZym/i/Qf5nQafjChCyKOEQKBgQD9f4SkvJrp/mFKWLHd\nkDvRpSIIltfJsa5KShn1IHsQXFwc0YgyP4SKQb3Ckv+/9UFHK9EzM+WlPxZi7ZOS\nhsWhIfkI4c4ORpxUQ+hPi0K2k+HIY7eYyONqDAzw5PGkKBo3mSGMHDXYywSqexhB\nCCMHuHdMhwyHdz4PWYOK3C2VMQKBgQDnpsrHK7lM9aVb8wNhTokbK5IlTSzH/5oJ\nlAVu6G6H3tM5YQeoDXztbZClvrvKU8DU5UzwaC+8AEWQwaram29QIDpAI3nVQQ0k\ndmHHp/pCeADdRG2whaGcl418UJMMv8AUpWTRm+kVLTLqfTHBC0ji4NlCQMHCUCfd\nU8TeUi5QBQKBgQDvJNd7mboDOUmLG7VgMetc0Y4T0EnuKsMjrlhimau/OYJkZX84\n+BcPXwmnf4nqC3Lzs3B9/12L0MJLvZjUSHQ0mJoZOPxtF0vvasjEEbp0B3qe0wOn\nDQ0NRCUJNNKJbJOfE8VEKnDZ/lx+f/XXk9eINwvElDrLqUBQtr+TxjbyYQKBgAxQ\nlZ8Y9/TbajsFJDzcC/XhzxckjyjisbGoqNFIkfevJNN8EQgiD24f0Py+swUChtHK\njtiI8WCxMwGLCiYs9THxRKd8O1HW73fswy32BBvcfU9F//7OW9UTSXY+YlLfLrrq\nP/3UqAN0L6y/kxGMJAfLpEEdaC+IS1Y8yc531/ZxAoGASYiasDpePtmzXklDxk3h\njEw64QAdXK2p/xTMjSeTtcqJ7fvaEbg+Mfpxq0mdTjfbTdR9U/nzAkwS7OoZZ4Du\nueMVls0IVqcNnBtikG8wgdxN27b5JPXS+GzQ0zDSpWFfRPZiIh37BAXr0D1voluJ\nrEHkcals6p7hL98BoxjFIvA=\n-----END PRIVATE KEY-----\n",
});
export const invalidTls = Object.freeze({
cert: "-----BEGIN CERTIFICATE-----\nBQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh\nBQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh\nbmNpc2NvMQ0wCwYDVQQKDARPdmVuMREwDwYDVQQLDAhUZWFtIEJ1bjETMBEGA1UE\nAwwKc2VydmVyLWJ1bjAeFw0yNTAyMDQwNDUyNTdaFw0yNzAyMDQwNDUyNTdaMGkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNj\nbzENMAsGA1UECgwET3ZlbjERMA8GA1UECwwIVGVhbSBCdW4xEzARBgNVBAMMCnNl\ncnZlci1idW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1rZqCnASs\nHPzPjs/mls+z3qTl6OsCNI+kTsA23/+ZkvtBe7EI+9LfV1Sy4MF66ZovR0UgeJUB\nlL7ExadXkfZJS0N6LEAIyEMQI0cpILv3i6sJCcRwHV7X7N55lkUdsJtQ3fSKsyn9\nPDWJGVdwtRjdod3XyevYcx5NLGZOF/4KJmR4eNkX8ycG8zvW/srPHHE95/+k/5Wo\n/RrS+OLl+bgVznxmXtnFMdbYvJ1RLyipCED2P569NWXAgCzYESX2tqLr20R8ca8Q\niTcXXijY1Wq+pVR5NhIckt+zyZlUQ5IT3DvAQn4aW30wA514k1AKDKQjtxdRzVmV\nGDOTOzAlpmeZAgMBAAGjTzBNMCwGA1UdEQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQ\nAAAAAAAAAAAAAAAAAAAAATAdBgNVHQ4EFgQUkgeZUw9BZc/9mxAym4BjaVYhHoow\nDQYJKoZIhvcNAQELBQADggEBAJGQomt68rO1wuhHaG355kGaIsTsoUJgs7VAKNI2\n0/vtMKODX2Zo2BHhiI1wSH751IySqWbGYCvXl6QrsV5tD/jdIYKvyXLFmV0KgQSY\nkZ91sde4jIiiqL5h03GJSUydCl4SE1A1H8Ht41NYoyVaWVPzv5lprt654vpRPbPq\nYBQTWSFcYkmGnza/tRALUftM5U3yKOTQ8sKH/eKGC9KU0DI5pZ2XAxrIyvrJZMm1\n0WwWTrO0KlXN8N9v8tVCVm7g6mYug4HEADQ4kymyfwM6mPY1EmsGy36KOqCRUtUR\n+jmAZr9m+l+27GxR9zjxoLWHkARuWZM/hL//u90cNfNDRgQ=\n-----END CERTIFICATE-----\n",
key: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1rZqCnASsHPzP\njs/mls+z3qTl6OsCNI+kTsA23/+ZkvtBe7EI+9LfV1Sy4MF66ZovR0UgeJUBlL7E\nxadXkfZJS0N6LEAIyEMQI0cpILv3i6sJCcRwHV7X7N55lkUdsJtQ3fSKsyn9PDWJ\nGVdwtRjdod3XyevYcx5NLGZOF/4KJmR4eNkX8ycG8zvW/srPHHE95/+k/5Wo/RrS\n+OLl+bgVznxmXtnFMdbYvJ1RLyipCED2P569NWXAgCzYESX2tqLr20R8ca8QiTcX\nXijY1Wq+pVR5NhIckt+zyZlUQ5IT3DvAQn4aW30wA514k1AKDKQjtxdRzVmVGDOT\nOzAlpmeZAgMBAAECggEANW6F2zTckOwDlF2pmmUvV/S6pZ1/hIoF1uqMUHdHmpim\nSaeBtSUu6x2pnORKMwaCILaCx55/IFRpWMDSywf0GbFHeqaJ/Ks9QgFGG/vzHEZY\n+pMDUX/p1XJmKfc+g5Fd1IY6thIkXsR28EfiNhUk54YEE0NhGCsfNc5BlmUrAzuw\nSevCkbChsZzLoasskt5hgWOb1wT757xDrOOss3LXvwaFkMXANQHiaGWxSpmyXTVf\nmtIX4wpN2K5BQxRBV6xmRaBBp7fWJlbqvV67wwh2cxIAyvQ68VVVHTbfv44TUw62\nyCKle6hSLi/OnMr1FJv16Ez+K3lUIkYE0nTYIvQkYwKBgQD34Nwy0U8WcHcOSbU1\nMIREfvPNYfl/hH94wmLyKZWhpqOfqgrUg+19+n9TXw4DGdO7lwAzz+ud7TImbGDI\n+1cb9/FxTK5cRwICTLC+Pse6pVkKUvPdil/TfHZBJP1jeIMGMDVi0fcGv97LxrHV\nJGQwA5x1nHGHl0JrENRqm3M2NwKBgQC7oXkWb0s2C8ANI14gz+q/2EmTSJxxrzXR\nz5EQk87PmPfkY4I1T8vKFcaiJynyReLwpYTip2WYGqc7qAO9oLwmA+d/NMOBI2sg\noEn154Q9zvr3jqIgu9/AapEgEDlA+v18veoIz3bae6wu57lpGvGtCoQLBS6q2UZg\n3zFI3BJorwKBgDz4WjFFuqZSU3Z4OtIydNZEQ8Oo7a2n8ZLKfXwDLoLsciK7uJ49\nNRVfoCHpp5CrsaDaq3oTEmluBn/c+JF3AR4oBoNP0TNxY9Uc9/xThN0r/pLDhKhh\neOCUJKIxbwIgilnjUb5U1uYaG7sTzHoY0Wvd94YWTPaFBhk/sn/mbJhRAoGAA+/E\nWZsmKdEfS2dFj0ytcS75hDSOy7fQWkGPmphvS127Pbh0v+eXr/q6+yX1NFcRBtmC\nKzs133YXsiG5Sl439Fg6oCmcPHZgxgN26cjctmtESrNcZXFrpV7XAqQ0f0+Ex/w4\nD81Cghz8JNPJyRG+plHFKXIHY6BBYMDuCMhNPpMCgYEA1BVG5scNtmBE3SaRns2G\npKgWiwmzPDTqwf3R0rgkHQroZUIz616jLgKXIVMBPaq/771uq+hzJZro9sNcNL8p\n9PkLRr4V4KtUSqkjvitU68vMM1qxtO9NVwCI5u3wicVC5mMqcH8FN+sO/5/jIPBl\nO/qEOVDlCuYtURcnh/Oz1cE=\n-----END PRIVATE KEY-----\n",
});
export function disableAggressiveGCScope() {
const gc = Bun.unsafe.gcAggressionLevel(0);
return {
[Symbol.dispose]() {
Bun.unsafe.gcAggressionLevel(gc);
},
};
}
String.prototype.isLatin1 = function () {
return require("bun:internal-for-testing").jscInternals.isLatin1String(this);
};
String.prototype.isUTF16 = function () {
return require("bun:internal-for-testing").jscInternals.isUTF16String(this);
};
interface BunHarnessTestMatchers {
toBeLatin1String(): void;
toBeUTF16String(): void;
toHaveTestTimedOutAfter(expected: number): void;
toBeBinaryType(expected: keyof typeof binaryTypes): void;
toRun(optionalStdout?: string, expectedCode?: number): void;
toThrowWithCode(cls: CallableFunction, code: string): void;
toThrowWithCodeAsync(cls: CallableFunction, code: string): Promise<void>;
}
declare module "bun:test" {
interface Matchers<T> extends BunHarnessTestMatchers {}
interface AsymmetricMatchers extends BunHarnessTestMatchers {}
}
/**
* Set `NODE_TLS_REJECT_UNAUTHORIZED` for a scope.
*/
export function rejectUnauthorizedScope(value: boolean) {
const original_rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = value ? "1" : "0";
return {
[Symbol.dispose]() {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = original_rejectUnauthorized;
},
};
}
let networkInterfaces: any;
function isIP(type: "IPv4" | "IPv6") {
if (!networkInterfaces) {
networkInterfaces = os.networkInterfaces();
}
for (const networkInterface of Object.values(networkInterfaces)) {
for (const { family } of networkInterface as any[]) {
if (family === type) {
return true;
}
}
}
return false;
}
export function isIPv6() {
// FIXME: AWS instances on Linux for Buildkite are not setup with IPv6
if (isBuildKite && isLinux) {
return false;
}
return isIP("IPv6");
}
export function isIPv4() {
return isIP("IPv4");
}
let glibcVersion: string | undefined;
export function getGlibcVersion() {
if (glibcVersion || !isLinux) {
return glibcVersion;
}
try {
const { header } = process.report!.getReport() as any;
const { glibcVersionRuntime: version } = header;
if (typeof version === "string") {
return (glibcVersion = version);
}
} catch (error) {
console.warn("Failed to detect glibc version", error);
}
}
export function isGlibcVersionAtLeast(version: string): boolean {
const glibcVersion = getGlibcVersion();
if (!glibcVersion) {
return false;
}
return Bun.semver.satisfies(glibcVersion, `>=${version}`);
}
let macOSVersion: string | undefined;
export function getMacOSVersion(): string | undefined {
if (macOSVersion || !isMacOS) {
return macOSVersion;
}
try {
const { stdout } = Bun.spawnSync({
cmd: ["sw_vers", "-productVersion"],
});
return (macOSVersion = stdout.toString().trim());
} catch (error) {
console.warn("Failed to detect macOS version:", error);
}
}
export function isMacOSVersionAtLeast(minVersion: number): boolean {
const macOSVersion = getMacOSVersion();
if (!macOSVersion) {
return false;
}
return parseFloat(macOSVersion) >= minVersion;
}
export function readableStreamFromArray(array) {
return new ReadableStream({
pull(controller) {
for (let entry of array) {
controller.enqueue(entry);
}
controller.close();
},
});
}
let hasGuardMalloc = -1;
export function forceGuardMalloc(env) {
if (process.platform !== "darwin") {
return;
}
if (hasGuardMalloc === -1) {
hasGuardMalloc = Number(fs.existsSync("/usr/lib/libgmalloc.dylib"));
}
if (hasGuardMalloc === 1) {
env.DYLD_INSERT_LIBRARIES = "/usr/lib/libgmalloc.dylib";
env.MALLOC_PROTECT_BEFORE = "1";
env.MallocScribble = "1";
env.MallocGuardEdges = "1";
env.MALLOC_FILL_SPACE = "1";
env.MALLOC_STRICT_SIZE = "1";
} else {
console.warn("Guard malloc is not available on this platform for some reason.");
}
}
export function fileDescriptorLeakChecker() {
const initial = getMaxFD();
return {
[Symbol.dispose]() {
const current = getMaxFD();
if (current > initial) {
throw new Error(`File descriptor leak detected: ${current} (current) > ${initial} (initial)`);
}
},
};
}
/**
* Gets a secret from the environment.
*
* In Buildkite, secrets must be retrieved using the `buildkite-agent secret get` command
* and are not available as an environment variable.
*/
export function getSecret(name: string): string | undefined {
let value = process.env[name]?.trim();
// When not running in CI, allow the secret to be missing.
if (!isCI) {
return value;
}
// In Buildkite, secrets must be retrieved using the `buildkite-agent secret get` command
if (!value && isBuildKite) {
const { exitCode, stdout } = spawnSync({
cmd: ["buildkite-agent", "secret", "get", name],
stdout: "pipe",
env: ciEnv,
stderr: "inherit",
});
if (exitCode === 0) {
value = stdout.toString().trim();
}
}
// Throw an error if the secret is not found, so the test fails in CI.
if (!value) {
let hint;
if (isBuildKite) {
hint = `Create a secret with the name "${name}" in the Buildkite UI.
https://buildkite.com/docs/pipelines/security/secrets/buildkite-secrets`;
} else {
hint = `Define an environment variable with the name "${name}".`;
}
throw new Error(`Secret not found: ${name}\n${hint}`);
}
// Set the secret in the environment so that it can be used in tests.
process.env[name] = value;
return value;
}
export function assertManifestsPopulated(absCachePath: string, registryUrl: string) {
const { npm_manifest_test_helpers } = require("bun:internal-for-testing");
const { parseManifest } = npm_manifest_test_helpers;
for (const file of fs.readdirSync(absCachePath)) {
if (!file.endsWith(".npm")) continue;
const manifest = parseManifest(join(absCachePath, file), registryUrl);
expect(manifest.versions.length).toBeGreaterThan(0);
}
}
// Make it easier to run some node tests.
Object.defineProperty(globalThis, "gc", {
value: Bun.gc,
writable: true,
enumerable: false,
configurable: true,
});
export function waitForFileToExist(path: string, interval_ms: number) {
while (!fs.existsSync(path)) {
sleepSync(interval_ms);
}
}
export function libcPathForDlopen() {
switch (process.platform) {
case "linux":
switch (libcFamily) {
case "glibc":
return "libc.so.6";
case "musl":
return "/usr/lib/libc.so";
}
case "darwin":
return "libc.dylib";
default:
throw new Error("TODO");
}
}
export function cwdScope(cwd: string) {
const original = process.cwd();
process.chdir(cwd);
return {
[Symbol.dispose]() {
process.chdir(original);
},
};
}
export function rmScope(path: string) {
return {
[Symbol.dispose]() {
fs.rmSync(path, { recursive: true, force: true });
},
};
}
export function textLockfile(version: number, pkgs: any): string {
return JSON.stringify({
lockfileVersion: version,
...pkgs,
});
}
export class VerdaccioRegistry {
port: number;
process: ChildProcess | undefined;
configPath: string;
packagesPath: string;
users: Record<string, string> = {};
constructor(opts?: { configPath?: string; packagesPath?: string; verbose?: boolean }) {
this.port = randomPort();
this.configPath = opts?.configPath ?? join(import.meta.dir, "cli", "install", "registry", "verdaccio.yaml");
this.packagesPath = opts?.packagesPath ?? join(import.meta.dir, "cli", "install", "registry", "packages");
}
async start(silent: boolean = true) {
await rm(join(dirname(this.configPath), "htpasswd"), { force: true });
this.process = fork(require.resolve("verdaccio/bin/verdaccio"), ["-c", this.configPath, "-l", `${this.port}`], {
silent,
// Prefer using a release build of Bun since it's faster
execPath: isCI ? bunExe() : Bun.which("bun") || bunExe(),
env: {
...(bunEnv as any),
NODE_NO_WARNINGS: "1",
},
});
this.process.stderr?.on("data", data => {
console.error(`[verdaccio] stderr: ${data}`);
});
const started = Promise.withResolvers();
this.process.on("error", error => {
console.error(`Failed to start verdaccio: ${error}`);
started.reject(error);
});
this.process.on("exit", (code, signal) => {
if (code !== 0) {
console.error(`Verdaccio exited with code ${code} and signal ${signal}`);
} else {
console.log("Verdaccio exited successfully");
}
});
this.process.on("message", (message: { verdaccio_started: boolean }) => {
if (message.verdaccio_started) {
started.resolve();
}
});
await started.promise;
}
registryUrl() {
return `http://localhost:${this.port}/`;
}
stop() {
rmSync(join(dirname(this.configPath), "htpasswd"), { force: true });
this.process?.kill(0);
}
/**
* returns auth token
*/
async generateUser(username: string, password: string): Promise<string> {
if (this.users[username]) {
throw new Error(`User ${username} already exists`);
} else this.users[username] = password;
const url = `http://localhost:${this.port}/-/user/org.couchdb.user:${username}`;
const user = {
name: username,
password: password,
email: `${username}@example.com`,
};
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(user),
});
if (response.ok) {
const data = await response.json();
return data.token;
}
throw new Error("Failed to create user:", response.statusText);
}
async authBunfig(user: string) {
const authToken = await this.generateUser(user, user);
return `
[install]
cache = false
registry = { url = "http://localhost:${this.port}/", token = "${authToken}" }
`;
}
async createTestDir(
opts: { bunfigOpts?: BunfigOpts; files?: DirectoryTree | string } = { bunfigOpts: {}, files: {} },
) {
await rm(join(dirname(this.configPath), "htpasswd"), { force: true });
await rm(join(this.packagesPath, "private-pkg-dont-touch"), { force: true });
const packageDir = tempDir("verdaccio-test-", opts.files ?? {});
const packageJson = join(packageDir, "package.json");
await this.writeBunfig(packageDir, opts.bunfigOpts);
this.users = {};
return { packageDir: String(packageDir), packageJson };
}
async writeBunfig(dir: string, opts: BunfigOpts = {}) {
let bunfig = `
[install]
cache = "${join(dir, ".bun-cache").replaceAll("\\", "\\\\")}"
`;
if ("saveTextLockfile" in opts) {
bunfig += `saveTextLockfile = ${opts.saveTextLockfile}
`;
}
if (!opts.npm) {
bunfig += `registry = "${this.registryUrl()}"\n`;
}
bunfig += `linker = "${opts.isolated ? "isolated" : "hoisted"}"\n`;
await write(join(dir, "bunfig.toml"), bunfig);
}
}
type BunfigOpts = {
saveTextLockfile?: boolean;
npm?: boolean;
isolated?: boolean;
};
export async function readdirSorted(path: string): Promise<string[]> {
const results = await readdir(path);
results.sort();
return results;
}
/**
* Helper function for making automatically lazily-executed promises.
*
* The difference is that the promise has not already started to be evaluated when it is created,
* only when you await it does it execute the function.
*
* @example
* ```ts
* function createMyFixture() {
* return {
* start: lazyPromiseLike(() => fetch("https://example.com")),
* stop: lazyPromiseLike(() => fetch("https://example.com")),
* }
* }
*
* const { start, stop } = createMyFixture();
*
* await start; // Calls only the start function
* ```
*
* @param fn A function to make lazily evaluated.
* @returns A promise-like object that will evaluate the function when `then` is called.
*/
export function lazyPromiseLike<T>(fn: () => Promise<T>): PromiseLike<T> {
let p: Promise<T>;
return {
then(onfulfilled, onrejected) {
if (!p) {
p = fn();
}
return p.then(onfulfilled, onrejected);
},
};
}
export async function gunzipJsonRequest(req: Request) {
const buf = await req.arrayBuffer();
const inflated = Bun.gunzipSync(buf);
const body = JSON.parse(Buffer.from(inflated).toString("utf-8"));
return body;
}
export function normalizeBunSnapshot(snapshot: string, optionalDir?: string) {
if (optionalDir) {
snapshot = snapshot
.replaceAll(fs.realpathSync.native(optionalDir).replaceAll("\\", "/"), "<dir>")
.replaceAll(optionalDir, "<dir>");
}
// Remove timestamps from test result lines that start with (pass), (fail), (skip), or (todo)
snapshot = snapshot.replace(/^((?:pass|fail|skip|todo)\) .+) \[[\d.]+\s?m?s\]$/gm, "$1");
return (
snapshot
.replaceAll("\r\n", "\n")
.replaceAll("\\", "/")
.replaceAll(fs.realpathSync.native(process.cwd()).replaceAll("\\", "/"), "<cwd>")
.replaceAll(fs.realpathSync.native(os.tmpdir()).replaceAll("\\", "/"), "<tmp>")
.replaceAll(fs.realpathSync.native(os.homedir()).replaceAll("\\", "/"), "<home>")
// look for [\d\d ms] or [\d\d s] with optional periods
.replace(/\s\[[\d.]+\s?m?s\]/gm, "")
.replace(/^\[[\d.]+\s?m?s\]/gm, "")
// line numbers in stack traces like at FunctionName (NN:NN)
// it must specifically look at the stacktrace format
.replace(/^\s+at (.*?)\(.*?:\d+(?::\d+)?\)/gm, " at $1(file:NN:NN)")
// Handle version strings in error messages like "Bun v1.2.21+revision (platform arch)"
// This needs to come before the other version replacements
.replace(/Bun v[\d.]+(?:-[\w.]+)?(?:\+[\w]+)?(?:\s+\([^)]+\))?/g, "Bun v<bun-version>")
.replaceAll(Bun.version_with_sha, "<version> (<revision>)")
.replaceAll(Bun.version, "<bun-version>")
.replaceAll(Bun.revision, "<revision>")
.trim()
);
}
export function nodeModulesPackages(nodeModulesPath: string): string {
const packages: string[] = [];
function scanDirectory(dir: string, relativePath: string = "") {
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === ".bun-cache") {
// Skip .bun-cache directories
continue;
}
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
// Check if this directory contains a package.json
const packageJsonPath = join(fullPath, "package.json");
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const name = packageJson.name || "unknown";
const version = packageJson.version || "unknown";
packages.push(`${newRelativePath}/${name}@${version}`);
} catch {
// If package.json is invalid, still include the path
packages.push(`${newRelativePath}/[invalid-package.json]`);
}
}
// Recursively scan subdirectories (including nested node_modules)
scanDirectory(fullPath, newRelativePath);
}
}
} catch (err) {
// Ignore errors for directories we can't read
}
}
scanDirectory(nodeModulesPath);
// Sort the packages alphabetically
packages.sort();
return packages.join("\n");
}