Files
bun.sh/test/cli/hot/hot.test.ts
Jarred Sumner 6184542682 Add BUN_DEBUG flag to control where debug logs go (#9019)
* 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>
2024-02-21 14:13:43 -08:00

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);
}
});