testServersStopWhenTheirTestDoes

tcmts.cli.test.e2e.harness.testServersStopWhenTheirTestDoes · #irx45flx · term

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

uses