mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
1975 lines
67 KiB
TypeScript
1975 lines
67 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";
|
|
|
|
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",
|
|
AGENT: "false",
|
|
};
|
|
|
|
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 }) => Bun.MaybePromise<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) {
|
|
if (isAbsolute(actual)) {
|
|
// junctions on windows will have an absolute path
|
|
return {
|
|
pass: actual.includes(expectedLinkPath.split("..").at(-1)!),
|
|
message,
|
|
};
|
|
}
|
|
|
|
return {
|
|
pass: actual === expectedLinkPath,
|
|
message,
|
|
};
|
|
}
|
|
|
|
const pass = actual === expectedLinkPath;
|
|
return { pass, message };
|
|
}
|
|
|
|
export function getFDCount(): number {
|
|
if (isMacOS || isLinux) {
|
|
return fs.readdirSync(isMacOS ? "/dev/fd" : "/proc/self/fd").length;
|
|
}
|
|
|
|
const maxFD = openSync("/dev/null", "r");
|
|
closeSync(maxFD);
|
|
return maxFD;
|
|
}
|
|
|
|
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,
|
|
configVersion: 1,
|
|
...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: { linker: "hoisted" },
|
|
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`;
|
|
}
|
|
if (opts.linker) {
|
|
bunfig += `linker = "${opts.linker}"\n`;
|
|
}
|
|
if (opts.publicHoistPattern) {
|
|
if (typeof opts.publicHoistPattern === "string") {
|
|
bunfig += `publicHoistPattern = "${opts.publicHoistPattern}"`;
|
|
} else {
|
|
bunfig += `publicHoistPattern = [${opts.publicHoistPattern.map(p => `"${p}"`).join(", ")}]`;
|
|
}
|
|
}
|
|
await write(join(dir, "bunfig.toml"), bunfig);
|
|
}
|
|
}
|
|
|
|
type BunfigOpts = {
|
|
saveTextLockfile?: boolean;
|
|
npm?: boolean;
|
|
linker?: "isolated" | "hoisted";
|
|
publicHoistPattern?: string | string[];
|
|
};
|
|
|
|
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;
|
|
}
|
|
/**
|
|
* HTML content from example.com, used as a test fixture to replace
|
|
* external calls to example.com with local server responses.
|
|
*/
|
|
export const exampleHtml = Buffer.from(
|
|
"PCFkb2N0eXBlIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPHRpdGxlPkV4YW1wbGUgRG9tYWluPC90aXRsZT4KCiAgICA8bWV0YSBjaGFyc2V0PSJ1dGYtOCIgLz4KICAgIDxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtdHlwZSIgY29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04IiAvPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xIiAvPgogICAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgICAgYm9keSB7CiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2YwZjBmMjsKICAgICAgICBtYXJnaW46IDA7CiAgICAgICAgcGFkZGluZzogMDsKICAgICAgICBmb250LWZhbWlseToKICAgICAgICAgIC1hcHBsZS1zeXN0ZW0sIHN5c3RlbS11aSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAiU2Vnb2UgVUkiLCAiT3BlbiBTYW5zIiwgIkhlbHZldGljYSBOZXVlIiwgSGVsdmV0aWNhLCBBcmlhbCwKICAgICAgICAgIHNhbnMtc2VyaWY7CiAgICAgIH0KICAgICAgZGl2IHsKICAgICAgICB3aWR0aDogNjAwcHg7CiAgICAgICAgbWFyZ2luOiA1ZW0gYXV0bzsKICAgICAgICBwYWRkaW5nOiAyZW07CiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2ZkZmRmZjsKICAgICAgICBib3JkZXItcmFkaXVzOiAwLjVlbTsKICAgICAgICBib3gtc2hhZG93OiAycHggM3B4IDdweCAycHggcmdiYSgwLCAwLCAwLCAwLjAyKTsKICAgICAgfQogICAgICBhOmxpbmssCiAgICAgIGE6dmlzaXRlZCB7CiAgICAgICAgY29sb3I6ICMzODQ4OGY7CiAgICAgICAgdGV4dC1kZWNvcmF0aW9uOiBub25lOwogICAgICB9CiAgICAgIEBtZWRpYSAobWF4LXdpZHRoOiA3MDBweCkgewogICAgICAgIGRpdiB7CiAgICAgICAgICBtYXJnaW46IDAgYXV0bzsKICAgICAgICAgIHdpZHRoOiBhdXRvOwogICAgICAgIH0KICAgICAgfQogICAgPC9zdHlsZT4KICA8L2hlYWQ+CgogIDxib2R5PgogICAgPGRpdj4KICAgICAgPGgxPkV4YW1wbGUgRG9tYWluPC9oMT4KICAgICAgPHA+CiAgICAgICAgVGhpcyBkb21haW4gaXMgZm9yIHVzZSBpbiBpbGx1c3RyYXRpdmUgZXhhbXBsZXMgaW4gZG9jdW1lbnRzLiBZb3UgbWF5IHVzZSB0aGlzIGRvbWFpbiBpbiBsaXRlcmF0dXJlIHdpdGhvdXQKICAgICAgICBwcmlvciBjb29yZGluYXRpb24gb3IgYXNraW5nIGZvciBwZXJtaXNzaW9uLgogICAgICA8L3A+CiAgICAgIDxwPjxhIGhyZWY9Imh0dHBzOi8vd3d3LmlhbmEub3JnL2RvbWFpbnMvZXhhbXBsZSI+TW9yZSBpbmZvcm1hdGlvbi4uLjwvYT48L3A+CiAgICA8L2Rpdj4KICA8L2JvZHk+CjwvaHRtbD4K",
|
|
"base64",
|
|
).toString();
|
|
/**
|
|
* Creates a local HTTP or HTTPS server serving example.com HTML content.
|
|
* Useful for testing without external network calls.
|
|
*
|
|
* @param protocol - Protocol to use: "https" (default) or "http"
|
|
* @returns Object with server URL, optional CA cert, server instance, and cleanup methods
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const site = exampleSite("http");
|
|
* const response = await fetch(site.url);
|
|
* site.stop();
|
|
* ```
|
|
*
|
|
* @example Using disposal pattern
|
|
* ```ts
|
|
* using site = exampleSite();
|
|
* await fetch(site.url, { tls: { ca: site.ca } });
|
|
* // server automatically stopped when scope exits
|
|
* ```
|
|
*/
|
|
export function exampleSite(protocol: "https" | "http" = "https") {
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
tls: protocol === "https" ? tls : undefined,
|
|
hostname: "127.0.0.1",
|
|
fetch(req) {
|
|
return new Response(exampleHtml, {
|
|
headers: {
|
|
"Content-Type": "text/html",
|
|
},
|
|
});
|
|
},
|
|
});
|
|
return {
|
|
url: server.url,
|
|
ca: protocol === "https" ? tls.cert : undefined,
|
|
server,
|
|
stop() {
|
|
return server.stop();
|
|
},
|
|
async [Symbol.asyncDispose]() {
|
|
await server.stop();
|
|
},
|
|
};
|
|
}
|
|
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");
|
|
}
|