test(process): replace no-output timer subprocess with spawn mock

This commit is contained in:
Peter Steinberger 2026-03-02 19:16:18 +00:00
parent fbb343ab30
commit 6add2bcc15
2 changed files with 73 additions and 32 deletions

View 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();
});
});

View File

@ -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)"],