From eeefa286b6838ff3f6f369fe6553b0553c989339 Mon Sep 17 00:00:00 2001 From: DuCanhGH <75556609+DuCanhGH@users.noreply.github.com> Date: Thu, 11 Jan 2024 18:32:24 +0700 Subject: [PATCH] fix(tests): fixed createInstance's error handling --- .../__tests__/test-utils/create-describe.ts | 6 +- .../test-utils/next-instance-base.ts | 35 ++++-- .../__tests__/test-utils/tree-kill.ts | 116 ++++++++++++++++++ 3 files changed, 146 insertions(+), 11 deletions(-) create mode 100644 packages/next-pwa/__tests__/test-utils/tree-kill.ts diff --git a/packages/next-pwa/__tests__/test-utils/create-describe.ts b/packages/next-pwa/__tests__/test-utils/create-describe.ts index 313e4452..fe8b03b8 100644 --- a/packages/next-pwa/__tests__/test-utils/create-describe.ts +++ b/packages/next-pwa/__tests__/test-utils/create-describe.ts @@ -35,9 +35,9 @@ const createNext = async (opts: NextTestOpts) => { await nextInstance.spawn(); return nextInstance; } catch (err) { - console.error(`failed to create next instance: ${err}, cliOutput: ${nextInstance?.cliOutput ?? "N/A"}`); + console.error(`failed to create next instance: ${err}, cliOutput:${nextInstance?.cliOutput ? `\n${nextInstance.cliOutput}` : "N/A"}`); try { - void nextInstance?.destroy(); + await nextInstance?.destroy(); } catch (err) { console.error("failed to clean up after failure", err); } @@ -52,7 +52,7 @@ export const createDescribe = (name: string, opts: NextTestOpts, fn: (args: { ne next = await createNext(opts); }); afterAll(async () => { - await next.destroy(); + await next?.destroy(); }); const nextProxy = new Proxy({} as NextInstance, { get(_target, property: keyof NextInstance) { diff --git a/packages/next-pwa/__tests__/test-utils/next-instance-base.ts b/packages/next-pwa/__tests__/test-utils/next-instance-base.ts index e372841f..70988254 100644 --- a/packages/next-pwa/__tests__/test-utils/next-instance-base.ts +++ b/packages/next-pwa/__tests__/test-utils/next-instance-base.ts @@ -9,6 +9,8 @@ import * as cheerio from "cheerio"; import fsExtra from "fs-extra"; import type { PackageJson } from "type-fest"; +import treeKill from "./tree-kill.ts"; + export interface NextInstanceOpts { skipInstall: boolean; dependencies?: PackageJson["dependencies"]; @@ -161,14 +163,31 @@ export abstract class NextInstance { throw new Error("next instance already destroyed"); } this._isDestroyed = true; - let exitResolve: () => void; - const exitPromise = new Promise((resolve) => { - exitResolve = resolve; - }); - this._process?.addListener("exit", () => exitResolve()); - this._process?.kill(); - await exitPromise; - this._process = undefined; + if (this._process) { + try { + let exitResolve: () => void; + const exitPromise = new Promise((resolve) => { + exitResolve = resolve; + }); + this._process.addListener("exit", () => exitResolve()); + await new Promise((resolve) => { + if (this._process?.pid) { + treeKill(this._process.pid, "SIGKILL", (err) => { + if (err) { + console.error("Failed to kill tree of process", this._process?.pid, "err:", err); + } + resolve(); + }); + } + }); + this._process.kill("SIGKILL"); + await exitPromise; + this._process = undefined; + console.log("Stopped next server"); + } catch (err) { + console.error("Failed to stop next server", err); + } + } await this.clean(); } public async fetch(pathname: string, init?: RequestInit) { diff --git a/packages/next-pwa/__tests__/test-utils/tree-kill.ts b/packages/next-pwa/__tests__/test-utils/tree-kill.ts new file mode 100644 index 00000000..d3daa244 --- /dev/null +++ b/packages/next-pwa/__tests__/test-utils/tree-kill.ts @@ -0,0 +1,116 @@ +import { ChildProcessWithoutNullStreams, exec, spawn } from "node:child_process"; + +type Callback = (error?: Error) => void; + +export default (pid: number, signal?: string | number, callback?: Callback) => { + if (typeof signal === "function" && callback === undefined) { + callback = signal; + signal = undefined; + } + + if (Number.isNaN(pid)) { + if (callback) { + return callback(new Error("pid must be a number")); + } + throw new Error("pid must be a number"); + } + + const tree = new Map([[pid, []]]); + const pidsToProcess = new Set([pid]); + + switch (process.platform) { + case "win32": + exec(`taskkill /pid ${pid} /T /F`, (err) => void (!!err && callback?.(err))); + break; + case "darwin": + buildProcessTree( + pid, + tree, + pidsToProcess, + (parentPid) => spawn("pgrep", ["-P", `${parentPid}`]), + () => void killAll(tree, signal, callback), + ); + break; + default: // Linux + buildProcessTree( + pid, + tree, + pidsToProcess, + (parentPid) => spawn("ps", ["-o", "pid", "--no-headers", "--ppid", `${parentPid}`]), + () => void killAll(tree, signal, callback), + ); + break; + } +}; + +function killAll(tree: Map, signal: string | number | undefined, callback: Callback | undefined) { + const killed: Record = {}; + try { + tree.forEach((tree_pid) => { + for (const pidpid of tree_pid) { + if (!killed[pidpid]) { + killPid(pidpid, signal); + killed[pidpid] = true; + } + } + }); + } catch (err) { + if (callback) { + return callback(err as Error | undefined); + } + throw err; + } + if (callback) { + return callback(); + } +} + +function killPid(pid: number, signal: string | number | undefined) { + try { + process.kill(pid, signal); + } catch (err) { + if ((err as any).code !== "ESRCH") { + throw err; + } + } +} + +function buildProcessTree( + parentPid: number, + tree: Map, + pidsToProcess: Set, + spawnChildProcessesList: (parentPid: number) => ChildProcessWithoutNullStreams, + cb: Callback | undefined, +) { + const ps = spawnChildProcessesList(parentPid); + let allData = ""; + ps.stdout.on("data", (data: Buffer) => { + allData += data.toString("ascii"); + }); + + const onClose = (code: number | null) => { + pidsToProcess.delete(parentPid); + + if (code !== null && code !== 0) { + // no more parent processes + if (pidsToProcess.size === 0) { + cb?.(undefined); + } + return; + } + + const matches = allData.match(/\d+/g); + + if (matches) { + for (const pidMatch of matches) { + const pid = parseInt(pidMatch, 10); + tree.get(parentPid)?.push(pid); + tree.set(pid, []); + pidsToProcess.add(pid); + buildProcessTree(pid, tree, pidsToProcess, spawnChildProcessesList, cb); + } + } + }; + + ps.on("close", onClose); +}