testServersStopWhenTheirTestDoes
A test's servers stop when it does, however it ends: one that fails midway stops them as it fails, and one that
hangs, killed by its suite's time limit (SIGKILL: its finally never runs), has them stopped by its watchdog
rather than left running for good. (This test is both: run again as a program of its own, with role saying
which, it keeps a tcm server and fails, or hangs.)
import { okIn, plainEnv, tcmBin, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness"; import assert from "node:assert/strict"; import { spawn } from "node:child_process"; export const testServersStopWhenTheirTestDoes = async (role?: string) => { if (role === "fails" || role === "hangs") return withChildren(async (keep) => { const dir = tempDir("kept server"); okIn(dir, "init"); const server = keep( spawn(process.execPath, [tcmBin(), "serve", "--port", "0", "--token", "t"], { cwd: dir, env: plainEnv(), }), ); let said = ""; await new Promise<void>((resolve, reject) => { server.stdout!.setEncoding("utf8").on("data", (d: string) => { said += d; if (/url\s+http/.test(said)) resolve(); }); server.on("exit", (code) => reject(new Error(`tcm serve exited ${code}: ${said}`))); }); console.log(`kept ${server.pid}`); // (long enough for the test watching to see it running: failing stops it) if (role === "fails") { await new Promise((r) => setTimeout(r, 1500)); throw new Error("failed midway"); } // (as a test that never finishes does) await new Promise(() => {}); }); const alive = (pid: number) => { try { process.kill(pid, 0); return true; } catch { return false; } }; const servers: number[] = []; try { for (const r of ["fails", "hangs"]) { // this test's own program (the runner, with its node options), as the other role const test = spawn(process.execPath, [...process.execArgv, process.argv[1]!, r], { stdio: ["ignore", "pipe", "pipe"], }); let out = ""; test.stdout.setEncoding("utf8").on("data", (d: string) => (out += d)); test.stderr.setEncoding("utf8").on("data", (d: string) => (out += d)); const ended = new Promise<number | null>((resolve) => test.on("exit", (code) => resolve(code)), ); const pid = await new Promise<number>((resolve, reject) => { const t = setInterval(() => { const m = /kept (\d+)/.exec(out); if (m) { clearInterval(t); resolve(Number(m[1])); } }, 50); void ended.then(() => { clearInterval(t); const m = /kept (\d+)/.exec(out); if (m) resolve(Number(m[1])); else reject(new Error(`the ${r} test ended before keeping a server:\n${out}`)); }); }); servers.push(pid); assert.ok(alive(pid), `the ${r} test's server is running`); // the suite's time limit, for the one that hangs if (r === "hangs") test.kill("SIGKILL"); const code = await ended; if (r === "fails") assert.match(out, /failed midway/); assert.notEqual(code, 0); const until = Date.now() + 15_000; while (alive(pid) && Date.now() < until) await new Promise((res) => setTimeout(res, 100)); assert.ok( !alive(pid), `the server the ${r} test kept (pid ${pid}) stopped when the test did`, ); } } finally { // (whatever this test found, it leaves nothing running) for (const pid of servers) if (alive(pid)) process.kill(pid, "SIGKILL"); } }