mirror of
https://github.com/oven-sh/bun
synced 2026-02-12 03:48:56 +00:00
* Add `BUN_DEBUG` flag to control where debug logs go * Update all the actions * Configure temp * use spawn instead of rm * Use CLOSE_RANGE_CLOEXEC * Make some tests more reproducible * Update hot.test.ts * Detect file descriptor leaks and wait for stdout * Update runner.node.mjs * Update preload.ts --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
308 lines
7.3 KiB
TypeScript
308 lines
7.3 KiB
TypeScript
import { spawn } from "bun";
|
|
import { beforeAll, beforeEach, expect, it } from "bun:test";
|
|
import { bunExe, bunEnv, tempDirWithFiles, bunRun, bunRunAsScript } from "harness";
|
|
import { cpSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, copyFileSync } from "fs";
|
|
import { join } from "path";
|
|
import { tmpdir } from "os";
|
|
|
|
let hotRunnerRoot: string = "",
|
|
cwd = "";
|
|
beforeEach(() => {
|
|
const hotPath = join(tmpdir(), "bun-hot-test-" + (Date.now() | 0) + "_" + Math.random().toString(36).slice(2));
|
|
hotRunnerRoot = join(hotPath, "hot-runner-root.js");
|
|
rmSync(hotPath, { recursive: true, force: true });
|
|
cpSync(import.meta.dir, hotPath, { recursive: true, force: true });
|
|
cwd = hotPath;
|
|
});
|
|
|
|
it("should hot reload when file is overwritten", async () => {
|
|
const root = hotRunnerRoot;
|
|
try {
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "inherit",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
var reloadCounter = 0;
|
|
|
|
async function onReload() {
|
|
writeFileSync(root, readFileSync(root, "utf-8"));
|
|
}
|
|
|
|
for await (const line of runner.stdout) {
|
|
var str = new TextDecoder().decode(line);
|
|
var any = false;
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
reloadCounter++;
|
|
|
|
if (reloadCounter === 3) {
|
|
runner.unref();
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
any = true;
|
|
}
|
|
|
|
if (any) await onReload();
|
|
}
|
|
|
|
expect(reloadCounter).toBe(3);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
});
|
|
|
|
it("should recover from errors", async () => {
|
|
const root = hotRunnerRoot;
|
|
try {
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
let reloadCounter = 0;
|
|
const input = readFileSync(root, "utf-8");
|
|
function onReloadGood() {
|
|
writeFileSync(root, input);
|
|
}
|
|
|
|
function onReloadError() {
|
|
writeFileSync(root, "throw new Error('error');\n");
|
|
}
|
|
|
|
var queue = [onReloadError, onReloadGood, onReloadError, onReloadGood];
|
|
var errors: string[] = [];
|
|
var onError: (...args: any[]) => void;
|
|
(async () => {
|
|
for await (let line of runner.stderr) {
|
|
var str = new TextDecoder().decode(line);
|
|
errors.push(str);
|
|
// @ts-ignore
|
|
onError && onError(str);
|
|
}
|
|
})();
|
|
|
|
for await (const line of runner.stdout) {
|
|
var str = new TextDecoder().decode(line);
|
|
var any = false;
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
reloadCounter++;
|
|
|
|
if (reloadCounter === 3) {
|
|
runner.unref();
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
any = true;
|
|
}
|
|
|
|
if (any) {
|
|
queue.shift()!();
|
|
await new Promise<void>((resolve, reject) => {
|
|
if (errors.length > 0) {
|
|
errors.length = 0;
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
onError = resolve;
|
|
});
|
|
|
|
queue.shift()!();
|
|
}
|
|
}
|
|
|
|
expect(reloadCounter).toBe(3);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
});
|
|
|
|
it("should not hot reload when a random file is written", async () => {
|
|
const root = hotRunnerRoot;
|
|
try {
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "inherit",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
let reloadCounter = 0;
|
|
const code = readFileSync(root, "utf-8");
|
|
async function onReload() {
|
|
writeFileSync(root + ".another.yet.js", code);
|
|
unlinkSync(root + ".another.yet.js");
|
|
}
|
|
var waiter = new Promise<void>((resolve, reject) => {
|
|
setTimeout(async () => {
|
|
resolve();
|
|
}, 50);
|
|
});
|
|
var finished = false;
|
|
await Promise.race([
|
|
waiter,
|
|
(async () => {
|
|
if (finished) {
|
|
return;
|
|
}
|
|
for await (const line of runner.stdout) {
|
|
if (finished) {
|
|
return;
|
|
}
|
|
|
|
var str = new TextDecoder().decode(line);
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
if (finished) {
|
|
return;
|
|
}
|
|
await onReload();
|
|
|
|
reloadCounter++;
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
}
|
|
}
|
|
})(),
|
|
]);
|
|
finished = true;
|
|
runner.kill(0);
|
|
runner.unref();
|
|
|
|
expect(reloadCounter).toBe(1);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
});
|
|
|
|
it("should hot reload when a file is deleted and rewritten", async () => {
|
|
try {
|
|
const root = hotRunnerRoot + ".tmp.js";
|
|
copyFileSync(hotRunnerRoot, root);
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "inherit",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
var reloadCounter = 0;
|
|
|
|
async function onReload() {
|
|
const contents = readFileSync(root, "utf-8");
|
|
rmSync(root);
|
|
writeFileSync(root, contents);
|
|
}
|
|
|
|
for await (const line of runner.stdout) {
|
|
var str = new TextDecoder().decode(line);
|
|
var any = false;
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
reloadCounter++;
|
|
|
|
if (reloadCounter === 3) {
|
|
runner.unref();
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
any = true;
|
|
}
|
|
|
|
if (any) await onReload();
|
|
}
|
|
rmSync(root);
|
|
expect(reloadCounter).toBe(3);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
});
|
|
|
|
it("should hot reload when a file is renamed() into place", async () => {
|
|
const root = hotRunnerRoot + ".tmp.js";
|
|
copyFileSync(hotRunnerRoot, root);
|
|
try {
|
|
var runner = spawn({
|
|
cmd: [bunExe(), "--hot", "run", root],
|
|
env: bunEnv,
|
|
cwd,
|
|
stdout: "pipe",
|
|
stderr: "inherit",
|
|
stdin: "ignore",
|
|
});
|
|
|
|
var reloadCounter = 0;
|
|
|
|
async function onReload() {
|
|
const contents = readFileSync(root, "utf-8");
|
|
rmSync(root + ".tmpfile", { force: true });
|
|
await 1;
|
|
writeFileSync(root + ".tmpfile", contents);
|
|
await 1;
|
|
rmSync(root);
|
|
await 1;
|
|
renameSync(root + ".tmpfile", root);
|
|
await 1;
|
|
}
|
|
|
|
for await (const line of runner.stdout) {
|
|
var str = new TextDecoder().decode(line);
|
|
var any = false;
|
|
for (let line of str.split("\n")) {
|
|
if (!line.includes("[#!root]")) continue;
|
|
reloadCounter++;
|
|
|
|
if (reloadCounter === 3) {
|
|
runner.unref();
|
|
runner.kill();
|
|
break;
|
|
}
|
|
|
|
expect(line).toContain(`[#!root] Reloaded: ${reloadCounter}`);
|
|
any = true;
|
|
}
|
|
|
|
if (any) await onReload();
|
|
}
|
|
rmSync(root);
|
|
expect(reloadCounter).toBe(3);
|
|
} finally {
|
|
// @ts-ignore
|
|
runner?.unref?.();
|
|
// @ts-ignore
|
|
runner?.kill?.(9);
|
|
}
|
|
});
|