





















11// Test Group Report tests cover test group report script behavior.
2-import { spawnSync } from "node:child_process";
2+import { spawn, spawnSync } from "node:child_process";
33import fs from "node:fs";
44import os from "node:os";
55import path from "node:path";
@@ -64,6 +64,21 @@ async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
6464throw new Error(`timed out waiting for pid ${pid} to exit`);
6565}
666667+function waitForChildClose(
68+child: ReturnType<typeof spawn>,
69+timeoutMs = 5_000,
70+): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
71+return new Promise((resolve, reject) => {
72+const timeout = setTimeout(() => {
73+reject(new Error("child did not close before timeout"));
74+}, timeoutMs);
75+child.once("close", (code, signal) => {
76+clearTimeout(timeout);
77+resolve({ code, signal });
78+});
79+});
80+}
81+6782function writeGroupedReport(filePath: string) {
6883fs.writeFileSync(
6984filePath,
@@ -723,6 +738,67 @@ describe("scripts/test-group-report child process guard", () => {
723738}
724739});
725740741+it("cleans process-group descendants before forwarding parent SIGTERM", async () => {
742+if (process.platform === "win32") {
743+return;
744+}
745+746+const tempDir = makeTempDir();
747+const childPidPath = path.join(tempDir, "child.pid");
748+const readyPath = path.join(tempDir, "child.ready");
749+const reportModuleUrl = pathToFileURL(path.resolve("scripts/test-group-report.mjs")).href;
750+let childPid: number | undefined;
751+let runner: ReturnType<typeof spawn> | undefined;
752+try {
753+const childScript = [
754+"const fs = require('node:fs');",
755+"process.on('SIGTERM', () => {});",
756+`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
757+"setInterval(() => {}, 1000);",
758+].join("\n");
759+const parentScript = [
760+"const { spawn } = require('node:child_process');",
761+`spawn(process.execPath, ["--eval", ${JSON.stringify(childScript)}], { stdio: "ignore" });`,
762+`require("node:fs").writeFileSync(${JSON.stringify(readyPath)}, "ready");`,
763+"process.on('SIGTERM', () => process.exit(0));",
764+"setInterval(() => {}, 1000);",
765+].join("\n");
766+const runnerScript = [
767+`import { spawnText } from ${JSON.stringify(reportModuleUrl)};`,
768+"await spawnText(",
769+" process.execPath,",
770+` ["--eval", ${JSON.stringify(parentScript)}],`,
771+" { cwd: process.cwd(), env: process.env, killGraceMs: 5_000, timeoutMs: 60_000 },",
772+");",
773+].join("\n");
774+775+runner = spawn(process.execPath, ["--input-type=module", "--eval", runnerScript], {
776+cwd: process.cwd(),
777+stdio: ["ignore", "ignore", "pipe"],
778+});
779+await waitForFile(readyPath, 2_000);
780+await waitForFile(childPidPath, 2_000);
781+childPid = Number.parseInt(fs.readFileSync(childPidPath, "utf8"), 10);
782+expect(isProcessAlive(childPid)).toBe(true);
783+784+runner.kill("SIGTERM");
785+786+await expect(waitForChildClose(runner)).resolves.toEqual({
787+code: null,
788+signal: "SIGTERM",
789+});
790+await waitForDead(childPid, 2_000);
791+} finally {
792+if (runner?.pid && isProcessAlive(runner.pid)) {
793+runner.kill("SIGKILL");
794+}
795+if (childPid !== undefined && isProcessAlive(childPid)) {
796+process.kill(childPid, "SIGKILL");
797+}
798+fs.rmSync(tempDir, { recursive: true, force: true });
799+}
800+});
801+726802it("finishes promptly when timed process-group descendants exit cleanly", async () => {
727803if (process.platform === "win32") {
728804return;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。