Compare commits

...

5 Commits

Author SHA1 Message Date
Jarred Sumner
96d67563bb Add missing await 2024-08-30 19:14:48 -07:00
Jarred Sumner
ff4d5a2ce7 Update hot.test.ts 2024-08-30 18:51:47 -07:00
Jarred Sumner
c18c209349 unref 2024-08-30 18:22:46 -07:00
Jarred Sumner
c53cf2248d Deflake watch test 2024-08-30 17:56:59 -07:00
Jarred Sumner
588c08b2e2 Deflake hot test 2024-08-30 17:55:04 -07:00
2 changed files with 284 additions and 232 deletions

View File

@@ -1,20 +1,35 @@
import { spawn } from "bun";
import { beforeEach, expect, it } from "bun:test";
import { bunExe, bunEnv, tmpdirSync, isDebug } from "harness";
import { bunExe, bunEnv, tmpdirSync, isDebug, tempDirWithFiles, isWindows } from "harness";
import { cpSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, copyFileSync } from "fs";
import { join } from "path";
const timeout = isDebug ? Infinity : 10_000;
const longTimeout = isDebug ? Infinity : 30_000;
const timeout = isDebug ? Infinity : 60_000;
const longTimeout = isDebug ? Infinity : 60_000;
let hotRunnerRoot: string = "",
cwd = "";
let _files: Record<string, string> | undefined;
function getFiles() {
if (!_files) {
_files = {
"hot-runner-imported.js": readFileSync(join(__dirname, "hot-runner-imported.js"), "utf-8"),
"hot-runner-root.js": readFileSync(join(__dirname, "hot-runner-root.js"), "utf-8"),
"hot-runner.js": readFileSync(join(__dirname, "hot-runner.js"), "utf-8"),
};
}
return _files;
}
let hotTestI = 0;
beforeEach(() => {
const hotPath = tmpdirSync();
hotRunnerRoot = join(hotPath, "hot-runner-root.js");
rmSync(hotPath, { recursive: true, force: true });
cpSync(import.meta.dir, hotPath, { recursive: true, force: true });
const hotPath = tempDirWithFiles(
"cli-hot-" + hotTestI++ + "-" + Math.random().toString(36).substring(2, 15),
getFiles(),
);
cwd = hotPath;
hotRunnerRoot = join(hotPath, "hot-runner.js");
});
it(
@@ -35,6 +50,9 @@ it(
async function onReload() {
writeFileSync(root, readFileSync(root, "utf-8"));
if (isWindows) {
await Bun.sleep(32);
}
}
var str = "";
@@ -240,7 +258,7 @@ it(
async function onReload() {
const contents = readFileSync(root, "utf-8");
rmSync(root);
rmSync(root, { force: true, recursive: true });
writeFileSync(root, contents);
}
@@ -267,13 +285,13 @@ it(
if (any) await onReload();
}
rmSync(root);
rmSync(root, { force: true, recursive: true });
expect(reloadCounter).toBe(3);
} finally {
// @ts-ignore
runner?.unref?.();
// @ts-ignore
runner?.kill?.(9);
runner?.kill?.();
}
},
timeout,
@@ -302,7 +320,7 @@ it(
await 1;
writeFileSync(root + ".tmpfile", contents);
await 1;
rmSync(root);
rmSync(root, { force: true, recursive: true });
await 1;
renameSync(root + ".tmpfile", root);
await 1;
@@ -331,13 +349,13 @@ it(
if (any) await onReload();
}
rmSync(root);
rmSync(root, { force: true, recursive: true });
expect(reloadCounter).toBe(3);
} finally {
// @ts-ignore
runner?.unref?.();
// @ts-ignore
runner?.kill?.(9);
runner?.kill?.();
}
},
timeout,
@@ -347,66 +365,74 @@ const comment_spam = ("//" + "B".repeat(2000) + "\n").repeat(1000);
it(
"should work with sourcemap generation",
async () => {
writeFileSync(
hotRunnerRoot,
`// source content
${comment_spam}
throw new Error('0');`,
);
await using runner = spawn({
cmd: [bunExe(), "--smol", "--hot", "run", hotRunnerRoot],
env: bunEnv,
cwd,
stdout: "ignore",
stderr: "pipe",
stdin: "ignore",
});
let reloadCounter = 0;
function onReload() {
const root = hotRunnerRoot;
try {
writeFileSync(
hotRunnerRoot,
root,
`// source content
${comment_spam}
${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`,
throw new Error('0');`,
);
}
let str = "";
outer: for await (const chunk of runner.stderr) {
str += new TextDecoder().decode(chunk);
var any = false;
if (!/error: .*[0-9]\n.*?\n/g.test(str)) continue;
let it = str.split("\n");
let line;
while ((line = it.shift())) {
if (!line.includes("error")) continue;
str = "";
if (reloadCounter === 50) {
runner.kill();
break;
var runner = spawn({
cmd: [bunExe(), "--smol", "--hot", "run", root],
env: bunEnv,
cwd,
stdout: "ignore",
stderr: "pipe",
stdin: "ignore",
});
let reloadCounter = 0;
async function onReload() {
writeFileSync(
root,
`// source content
${comment_spam}
${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`,
);
if (isWindows) {
await Bun.sleep(32);
}
if (line.includes(`error: ${reloadCounter - 1}`)) {
onReload(); // re-save file to prevent deadlock
continue outer;
}
expect(line).toContain(`error: ${reloadCounter}`);
reloadCounter++;
let next = it.shift()!;
if (!next) throw new Error(line);
const match = next.match(/\s*at.*?:1003:(\d+)$/);
if (!match) throw new Error("invalid string: " + next);
const col = match[1];
expect(Number(col)).toBe(1 + "throw ".length + (reloadCounter - 1) * 2);
any = true;
}
let str = "";
outer: for await (const chunk of runner.stderr) {
str += new TextDecoder().decode(chunk);
var any = false;
if (!/error: .*[0-9]\n.*?\n/g.test(str)) continue;
if (any) await onReload();
let it = str.split("\n");
let line;
while ((line = it.shift())) {
if (!line.includes("error")) continue;
str = "";
if (reloadCounter === 50) {
runner.kill();
break;
}
if (line.includes(`error: ${reloadCounter - 1}`)) {
onReload(); // re-save file to prevent deadlock
continue outer;
}
expect(line).toContain(`error: ${reloadCounter}`);
reloadCounter++;
let next = it.shift()!;
if (!next) throw new Error(line);
const match = next.match(/\s*at.*?:1003:(\d+)$/);
if (!match) throw new Error("invalid string: " + next);
const col = match[1];
expect(Number(col)).toBe(1 + "throw ".length + (reloadCounter - 1) * 2);
any = true;
}
if (any) await onReload();
}
await runner.exited;
expect(reloadCounter).toBe(50);
} finally {
runner?.kill?.();
}
await runner.exited;
expect(reloadCounter).toBe(50);
},
timeout,
);
@@ -414,8 +440,9 @@ ${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`,
it(
"should work with sourcemap loading",
async () => {
const root = hotRunnerRoot;
let bundleIn = join(cwd, "bundle_in.ts");
rmSync(hotRunnerRoot);
rmSync(root, { force: true, recursive: true });
writeFileSync(
bundleIn,
`// source content
@@ -423,67 +450,74 @@ it(
//
throw new Error('0');`,
);
await using bundler = spawn({
cmd: [bunExe(), "build", "--watch", bundleIn, "--target=bun", "--sourcemap", "--outfile", hotRunnerRoot],
env: bunEnv,
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "ignore",
});
await using runner = spawn({
cmd: [bunExe(), "--hot", "run", hotRunnerRoot],
env: bunEnv,
cwd,
stdout: "ignore",
stderr: "pipe",
stdin: "ignore",
});
let reloadCounter = 0;
function onReload() {
writeFileSync(
bundleIn,
`// source content
try {
var bundler = spawn({
cmd: [bunExe(), "build", "--watch", bundleIn, "--target=bun", "--sourcemap", "--outfile", root],
env: bunEnv,
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "ignore",
});
var runner = spawn({
cmd: [bunExe(), "--hot", "run", root],
env: bunEnv,
cwd,
stdout: "ignore",
stderr: "pipe",
stdin: "ignore",
});
let reloadCounter = 0;
async function onReload() {
writeFileSync(
bundleIn,
`// source content
// etc etc
// etc etc
${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`,
);
}
let str = "";
outer: for await (const chunk of runner.stderr) {
str += new TextDecoder().decode(chunk);
var any = false;
if (!/error: .*[0-9]\n.*?\n/g.test(str)) continue;
let it = str.split("\n");
let line;
while ((line = it.shift())) {
if (!line.includes("error")) continue;
str = "";
if (reloadCounter === 50) {
runner.kill();
break;
);
if (isWindows) {
await Bun.sleep(32);
}
if (line.includes(`error: ${reloadCounter - 1}`)) {
onReload(); // re-save file to prevent deadlock
continue outer;
}
expect(line).toContain(`error: ${reloadCounter}`);
reloadCounter++;
let next = it.shift()!;
expect(next).toInclude("bundle_in.ts");
const col = next.match(/\s*at.*?:4:(\d+)$/)![1];
expect(Number(col)).toBe(1 + "throw ".length + (reloadCounter - 1) * 2);
any = true;
}
let str = "";
outer: for await (const chunk of runner.stderr) {
str += new TextDecoder().decode(chunk);
var any = false;
if (!/error: .*[0-9]\n.*?\n/g.test(str)) continue;
if (any) await onReload();
let it = str.split("\n");
let line;
while ((line = it.shift())) {
if (!line.includes("error")) continue;
str = "";
if (reloadCounter === 50) {
runner.kill();
break;
}
if (line.includes(`error: ${reloadCounter - 1}`)) {
await onReload(); // re-save file to prevent deadlock
continue outer;
}
expect(line).toContain(`error: ${reloadCounter}`);
reloadCounter++;
let next = it.shift()!;
expect(next).toInclude("bundle_in.ts");
const col = next.match(/\s*at.*?:4:(\d+)$/)![1];
expect(Number(col)).toBe(1 + "throw ".length + (reloadCounter - 1) * 2);
any = true;
}
if (any) await onReload();
}
expect(reloadCounter).toBe(50);
} finally {
bundler?.kill?.();
runner?.kill?.();
}
expect(reloadCounter).toBe(50);
bundler.kill();
},
timeout,
);
@@ -493,8 +527,9 @@ const long_comment = "BBBB".repeat(100000);
it(
"should work with sourcemap loading with large files",
async () => {
const root = hotRunnerRoot;
let bundleIn = join(cwd, "bundle_in.ts");
rmSync(hotRunnerRoot);
rmSync(root, { force: true, recursive: true });
writeFileSync(
bundleIn,
`// ${long_comment}
@@ -502,94 +537,102 @@ it(
console.error("RSS: %s", process.memoryUsage().rss);
throw new Error('0');`,
);
await using bundler = spawn({
cmd: [
//
bunExe(),
"build",
"--watch",
bundleIn,
"--target=bun",
"--sourcemap",
"--outfile",
hotRunnerRoot,
],
env: bunEnv,
cwd,
stdout: "ignore",
stderr: "ignore",
stdin: "ignore",
});
await using runner = spawn({
cmd: [
//
bunExe(),
"--hot",
"run",
hotRunnerRoot,
],
env: bunEnv,
cwd,
stdout: "inherit",
stderr: "pipe",
stdin: "ignore",
});
let reloadCounter = 0;
function onReload() {
writeFileSync(
bundleIn,
`// ${long_comment}
try {
var bundler = spawn({
cmd: [
//
bunExe(),
"build",
"--watch",
bundleIn,
"--target=bun",
"--sourcemap",
"--outfile",
root,
],
env: bunEnv,
cwd,
stdout: "ignore",
stderr: "ignore",
stdin: "ignore",
});
var runner = spawn({
cmd: [
//
bunExe(),
"--hot",
"run",
root,
],
env: bunEnv,
cwd,
stdout: "inherit",
stderr: "pipe",
stdin: "ignore",
});
let reloadCounter = 0;
async function onReload() {
writeFileSync(
bundleIn,
`// ${long_comment}
console.error("RSS: %s", process.memoryUsage().rss);
//
${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`,
);
}
let str = "";
let sampleMemory10: number | undefined;
let sampleMemory100: number | undefined;
outer: for await (const chunk of runner.stderr) {
str += new TextDecoder().decode(chunk);
var any = false;
if (!/error: .*[0-9]\n.*?\n/g.test(str)) continue;
let it = str.split("\n");
let line;
while ((line = it.shift())) {
if (!line.includes("error:")) continue;
let rssMatch = str.match(/RSS: (\d+(\.\d+)?)\n/);
let rss;
if (rssMatch) rss = Number(rssMatch[1]);
str = "";
if (reloadCounter == 10) {
sampleMemory10 = rss;
);
if (isWindows) {
await Bun.sleep(32);
}
if (reloadCounter >= 50) {
sampleMemory100 = rss;
runner.kill();
break;
}
if (line.includes(`error: ${reloadCounter - 1}`)) {
onReload(); // re-save file to prevent deadlock
continue outer;
}
expect(line).toContain(`error: ${reloadCounter}`);
reloadCounter++;
let next = it.shift()!;
expect(next).toInclude("bundle_in.ts");
const col = next.match(/\s*at.*?:4:(\d+)$/)![1];
expect(Number(col)).toBe(1 + "throw ".length + (reloadCounter - 1) * 2);
any = true;
}
let str = "";
let sampleMemory10: number | undefined;
let sampleMemory100: number | undefined;
outer: for await (const chunk of runner.stderr) {
str += new TextDecoder().decode(chunk);
var any = false;
if (!/error: .*[0-9]\n.*?\n/g.test(str)) continue;
if (any) await onReload();
let it = str.split("\n");
let line;
while ((line = it.shift())) {
if (!line.includes("error:")) continue;
let rssMatch = str.match(/RSS: (\d+(\.\d+)?)\n/);
let rss;
if (rssMatch) rss = Number(rssMatch[1]);
str = "";
if (reloadCounter == 10) {
sampleMemory10 = rss;
}
if (reloadCounter >= 50) {
sampleMemory100 = rss;
runner.kill();
break;
}
if (line.includes(`error: ${reloadCounter - 1}`)) {
await onReload(); // re-save file to prevent deadlock
continue outer;
}
expect(line).toContain(`error: ${reloadCounter}`);
reloadCounter++;
let next = it.shift()!;
expect(next).toInclude("bundle_in.ts");
const col = next.match(/\s*at.*?:4:(\d+)$/)![1];
expect(Number(col)).toBe(1 + "throw ".length + (reloadCounter - 1) * 2);
any = true;
}
if (any) await onReload();
}
expect(reloadCounter).toBe(50);
bundler.kill();
await runner.exited;
} finally {
bundler?.kill?.();
runner?.kill?.();
}
expect(reloadCounter).toBe(50);
bundler.kill();
await runner.exited;
// TODO: bun has a memory leak when --hot is used on very large files
// console.log({ sampleMemory10, sampleMemory100 });
},

View File

@@ -4,46 +4,55 @@ import { writeFile } from "node:fs/promises";
import { bunEnv, bunExe, forEachLine, tempDirWithFiles } from "harness";
import { join } from "node:path";
var watchTestI = 0;
describe("--watch works", async () => {
for (const watchedFile of ["tmp.js", "entry.js"]) {
test(`with ${watchedFile}`, async () => {
const tmpdir_ = tempDirWithFiles("watch-fixture", {
"tmp.js": "console.log('hello #1')",
"entry.js": "import './tmp.js'",
"package.json": JSON.stringify({ name: "foo", version: "0.0.1" }),
});
const tmpdir_ = tempDirWithFiles(
"cli-watch-" + watchTestI++ + "-" + Math.random().toString(36).substring(2, 15),
{
"tmp.js": "console.log('hello #1')",
"entry.js": "import './tmp.js'",
"package.json": JSON.stringify({ name: "foo", version: "0.0.1" }),
},
);
const tmpfile = join(tmpdir_, "tmp.js");
const process = spawn({
cmd: [bunExe(), "--watch", join(tmpdir_, watchedFile)],
cwd: tmpdir_,
env: bunEnv,
stdio: ["ignore", "pipe", "inherit"],
});
const { stdout } = process;
try {
var process = spawn({
cmd: [bunExe(), "--watch", join(tmpdir_, watchedFile)],
cwd: tmpdir_,
env: bunEnv,
stdio: ["ignore", "pipe", "inherit"],
});
process.unref();
const { stdout } = process;
const iter = forEachLine(stdout);
let { value: line, done } = await iter.next();
expect(done).toBe(false);
expect(line).toBe("hello #1");
const iter = forEachLine(stdout);
let { value: line, done } = await iter.next();
expect(done).toBe(false);
expect(line).toBe("hello #1");
await writeFile(tmpfile, "console.log('hello #2')");
({ value: line } = await iter.next());
expect(line).toBe("hello #2");
await writeFile(tmpfile, "console.log('hello #2')");
({ value: line } = await iter.next());
expect(line).toBe("hello #2");
await writeFile(tmpfile, "console.log('hello #3')");
({ value: line } = await iter.next());
expect(line).toBe("hello #3");
await writeFile(tmpfile, "console.log('hello #3')");
({ value: line } = await iter.next());
expect(line).toBe("hello #3");
await writeFile(tmpfile, "console.log('hello #4')");
({ value: line } = await iter.next());
expect(line).toBe("hello #4");
await writeFile(tmpfile, "console.log('hello #4')");
({ value: line } = await iter.next());
expect(line).toBe("hello #4");
await writeFile(tmpfile, "console.log('hello #5')");
({ value: line } = await iter.next());
expect(line).toBe("hello #5");
await writeFile(tmpfile, "console.log('hello #5')");
({ value: line } = await iter.next());
expect(line).toBe("hello #5");
process.kill("SIGKILL");
await process.exited;
process.kill?.();
await process.exited;
} finally {
process?.kill?.();
}
});
}
});