| 1 | import { expect, test } from "bun:test"; |
| 2 | import { spawn } from "node:child_process"; |
| 3 | import { |
| 4 | existsSync, |
| 5 | mkdtempSync, |
| 6 | readFileSync, |
| 7 | rmSync, |
| 8 | writeFileSync, |
| 9 | } from "node:fs"; |
| 10 | import { tmpdir } from "node:os"; |
| 11 | import { join, resolve } from "node:path"; |
| 12 | import { pollUntil } from "../src/DevCli.ts"; |
| 13 | |
| 14 | for (const signal of ["SIGINT", "SIGTERM"] as const) { |
| 15 | test(`forwards ${signal} to DevCli and waits for cleanup`, async () => { |
| 16 | const root = mkdtempSync(join(tmpdir(), "alchemy-forward-signals-")); |
| 17 | const devCli = resolve(import.meta.dirname, "../src/DevCli.ts"); |
| 18 | writeFileSync( |
| 19 | join(root, "cli.ts"), |
| 20 | ` |
| 21 | import { writeFileSync } from "node:fs"; |
| 22 | process.on(${JSON.stringify(signal)}, () => { |
| 23 | setTimeout(() => { writeFileSync("cleaned", "yes"); process.exit(0); }, 200); |
| 24 | }); |
| 25 | writeFileSync("child.pid", String(process.pid)); |
| 26 | setInterval(() => {}, 1000); |
| 27 | `, |
| 28 | ); |
| 29 | writeFileSync( |
| 30 | join(root, "owner.test.ts"), |
| 31 | ` |
| 32 | import { afterAll, test } from "bun:test"; |
| 33 | import { writeFileSync } from "node:fs"; |
| 34 | import { DevCli } from ${JSON.stringify(devCli)}; |
| 35 | const cli = new DevCli({ root: process.cwd(), alchemyBin: process.cwd() + "/cli.ts" }); |
| 36 | afterAll(async () => { writeFileSync("afterAll", "yes"); await cli.stop(); }); |
| 37 | test("running", async () => { cli.start(); await Bun.sleep(60000); }, 65000); |
| 38 | `, |
| 39 | ); |
| 40 | const runner = spawn(process.execPath, ["test", "owner.test.ts"], { |
| 41 | cwd: root, |
| 42 | detached: true, |
| 43 | stdio: "ignore", |
| 44 | env: { ...process.env, ALCHEMY_PROFILE: "alchemy" }, |
| 45 | }); |
| 46 | const exited = new Promise<number | null>((resolve, reject) => { |
| 47 | runner.once("exit", resolve); |
| 48 | runner.once("error", reject); |
| 49 | }); |
| 50 | const timeout = setTimeout(() => runner.kill("SIGKILL"), 6000); |
| 51 | let childPid: number | undefined; |
| 52 | try { |
| 53 | await pollUntil( |
| 54 | "CLI started", |
| 55 | () => (existsSync(join(root, "child.pid")) ? true : undefined), |
| 56 | { tries: 80, delayMs: 50 }, |
| 57 | ); |
| 58 | childPid = Number(readFileSync(join(root, "child.pid"), "utf8")); |
| 59 | process.kill(-runner.pid!, signal); |
| 60 | // A launcher can forward the same terminal signal more than once. |
| 61 | await Bun.sleep(50); |
| 62 | process.kill(-runner.pid!, signal); |
| 63 | expect(await exited).toBe(signal === "SIGINT" ? 130 : 143); |
| 64 | expect(existsSync(join(root, "cleaned"))).toBe(true); |
| 65 | expect(existsSync(join(root, "afterAll"))).toBe(false); |
| 66 | expect(() => process.kill(childPid!, 0)).toThrow(); |
| 67 | } finally { |
| 68 | clearTimeout(timeout); |
| 69 | runner.kill("SIGKILL"); |
| 70 | if (childPid !== undefined) { |
| 71 | try { |
| 72 | process.kill(childPid, "SIGKILL"); |
| 73 | } catch (error) { |
| 74 | if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; |
| 75 | } |
| 76 | } |
| 77 | rmSync(root, { recursive: true, force: true }); |
| 78 | } |
| 79 | }, 10000); |
| 80 | } |