test(process): replace no-output timer subprocess with spawn mock
This commit is contained in:
73
src/process/exec.no-output-timer.test.ts
Normal file
73
src/process/exec.no-output-timer.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const spawnMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("node:child_process", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
|
||||
return {
|
||||
...actual,
|
||||
spawn: spawnMock,
|
||||
};
|
||||
});
|
||||
|
||||
import { runCommandWithTimeout } from "./exec.js";
|
||||
|
||||
function createFakeSpawnedChild() {
|
||||
const child = new EventEmitter() as EventEmitter & ChildProcess;
|
||||
const stdout = new EventEmitter();
|
||||
const stderr = new EventEmitter();
|
||||
let killed = false;
|
||||
const kill = vi.fn<(signal?: NodeJS.Signals) => boolean>(() => {
|
||||
killed = true;
|
||||
return true;
|
||||
});
|
||||
Object.defineProperty(child, "killed", {
|
||||
get: () => killed,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(child, "pid", {
|
||||
value: 12345,
|
||||
configurable: true,
|
||||
});
|
||||
child.stdout = stdout as ChildProcess["stdout"];
|
||||
child.stderr = stderr as ChildProcess["stderr"];
|
||||
child.stdin = null;
|
||||
child.kill = kill as ChildProcess["kill"];
|
||||
return { child, stdout, stderr, kill };
|
||||
}
|
||||
|
||||
describe("runCommandWithTimeout no-output timer", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("resets no-output timeout when spawned child keeps emitting stdout", async () => {
|
||||
vi.useFakeTimers();
|
||||
const fake = createFakeSpawnedChild();
|
||||
spawnMock.mockReturnValue(fake.child);
|
||||
|
||||
const runPromise = runCommandWithTimeout(["node", "-e", "ignored"], {
|
||||
timeoutMs: 1_000,
|
||||
noOutputTimeoutMs: 80,
|
||||
});
|
||||
|
||||
fake.stdout.emit("data", Buffer.from("."));
|
||||
await vi.advanceTimersByTimeAsync(40);
|
||||
fake.stdout.emit("data", Buffer.from("."));
|
||||
await vi.advanceTimersByTimeAsync(40);
|
||||
fake.stdout.emit("data", Buffer.from("."));
|
||||
await vi.advanceTimersByTimeAsync(20);
|
||||
|
||||
fake.child.emit("close", 0, null);
|
||||
const result = await runPromise;
|
||||
|
||||
expect(result.code ?? 0).toBe(0);
|
||||
expect(result.termination).toBe("exit");
|
||||
expect(result.noOutputTimedOut).toBe(false);
|
||||
expect(result.stdout).toBe("...");
|
||||
expect(fake.kill).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -56,38 +56,6 @@ describe("runCommandWithTimeout", () => {
|
||||
expect(result.code).not.toBe(0);
|
||||
});
|
||||
|
||||
it("resets no output timer when command keeps emitting output", async () => {
|
||||
const result = await runCommandWithTimeout(
|
||||
[
|
||||
process.execPath,
|
||||
"-e",
|
||||
[
|
||||
"let count = 0;",
|
||||
"const emit = () => {",
|
||||
'process.stdout.write(".");',
|
||||
"count += 1;",
|
||||
"if (count >= 4) {",
|
||||
"process.exit(0);",
|
||||
"return;",
|
||||
"}",
|
||||
"setTimeout(emit, 40);",
|
||||
"};",
|
||||
"emit();",
|
||||
].join(" "),
|
||||
],
|
||||
{
|
||||
timeoutMs: 2_000,
|
||||
// Keep a healthy margin above the emit interval for loaded CI runners.
|
||||
noOutputTimeoutMs: 400,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.code ?? 0).toBe(0);
|
||||
expect(result.termination).toBe("exit");
|
||||
expect(result.noOutputTimedOut).toBe(false);
|
||||
expect(result.stdout.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it("reports global timeout termination when overall timeout elapses", async () => {
|
||||
const result = await runCommandWithTimeout(
|
||||
[process.execPath, "-e", "setTimeout(() => {}, 10)"],
|
||||
|
||||
Reference in New Issue
Block a user