mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
test: bound fixture child processes by the remaining test deadline
A processTest child run without its own timeoutMs now gets the test's remaining deadline less a reporting reserve (at least 5 seconds, or a tenth of the deadline). A hung child still fails with its captured output, without a fixed per-call value that assumes a host's speed. Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b158b2d1d
commit
077bebe49b
3 files changed
+79
-8
No files matched your search
@@ -307,7 +307,10 @@ assertion. `vitest.config.ts` applies the deadlines in
|
||||
and 120 seconds for built projects, with hooks sharing their project's
|
||||
deadline. Coverage doubles every deadline, and so does hosted CI, where
|
||||
CPU-bound cases share every core with other workers and their subprocesses.
|
||||
Do not pass a per-test timeout to fit a slow host. Tag a bounded workload that needs longer with
|
||||
Do not pass a per-test timeout to fit a slow host. A child started through
|
||||
the `processTest` registry, including `cli.run`, gets the test's remaining
|
||||
deadline less a reporting reserve, so a hung child fails with its captured
|
||||
output. Pass `timeoutMs` only when the child's deadline is itself under test. Tag a bounded workload that needs longer with
|
||||
`{ tags: ["long-running"] }`, or a whole file with
|
||||
`// @module-tag long-running`. Assert elapsed time explicitly when speed is the
|
||||
behavior under test. Derive an explicit deadline only when a test must outlive
|
||||
|
||||
@@ -6,7 +6,11 @@ import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { expect, it, vi } from "vitest";
|
||||
|
||||
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
|
||||
import { createTestProcessRegistry } from "../../support/process/processFixture.js";
|
||||
import {
|
||||
createTestProcessRegistry,
|
||||
processTest,
|
||||
remainingProcessDeadline,
|
||||
} from "../../support/process/processFixture.js";
|
||||
|
||||
const within = async <T>(
|
||||
operation: Promise<T>,
|
||||
@@ -277,3 +281,34 @@ it("terminates a stalled real child at its deadline while retaining captured out
|
||||
await processes.shutdown();
|
||||
}
|
||||
});
|
||||
|
||||
it("derives a child deadline from the test deadline, keeping a reporting reserve", () => {
|
||||
expect(remainingProcessDeadline(120_000, 0)).toBe(108_000);
|
||||
expect(remainingProcessDeadline(30_000, 20_000)).toBe(5_000);
|
||||
// The reserve never drops below five seconds, nor the deadline below one.
|
||||
expect(remainingProcessDeadline(8_000, 0)).toBe(3_000);
|
||||
expect(remainingProcessDeadline(8_000, 7_500)).toBe(1_000);
|
||||
expect(remainingProcessDeadline(0, 0)).toBeUndefined();
|
||||
});
|
||||
|
||||
processTest(
|
||||
"stops a hung child before its test deadline and reports its output",
|
||||
// The test deadline is the behavior under test: the child gets what remains.
|
||||
{ timeout: 8_000 },
|
||||
async ({ processes }) => {
|
||||
const started = performance.now();
|
||||
const failure: unknown = await processes
|
||||
.run(process.execPath, [
|
||||
"-e",
|
||||
"process.stdout.write('started'); setInterval(() => {}, 1_000);",
|
||||
])
|
||||
.catch((error: unknown) => error);
|
||||
expect(failure).toMatchObject({
|
||||
message: expect.stringMatching(
|
||||
/^Test process exceeded its \d+ms deadline$/u,
|
||||
),
|
||||
cause: { stdout: "started" },
|
||||
});
|
||||
expect(performance.now() - started).toBeLessThan(6_000);
|
||||
},
|
||||
);
|
||||
@@ -8,6 +8,26 @@ import {
|
||||
import { test as base } from "vitest";
|
||||
|
||||
const SHUTDOWN_GRACE_MS = 1_000;
|
||||
/** Minimum time kept back from a test deadline to stop a child and report it. */
|
||||
const MIN_REPORT_RESERVE_MS = 5_000;
|
||||
|
||||
/**
|
||||
* Default child deadline: the test's remaining deadline, less a reserve for
|
||||
* stopping the child and reporting its captured output. A hung child then fails
|
||||
* with its output instead of an opaque test timeout, without a fixed value that
|
||||
* assumes a host's speed. Undefined when the test itself has no deadline.
|
||||
*/
|
||||
export const remainingProcessDeadline = (
|
||||
testDeadlineMs: number,
|
||||
elapsedMs: number,
|
||||
): number | undefined => {
|
||||
if (!(testDeadlineMs > 0)) return undefined;
|
||||
const reserve = Math.max(MIN_REPORT_RESERVE_MS, testDeadlineMs / 10);
|
||||
return Math.max(
|
||||
SHUTDOWN_GRACE_MS,
|
||||
Math.floor(testDeadlineMs - elapsedMs - reserve),
|
||||
);
|
||||
};
|
||||
|
||||
/** Captured output and termination state from one child process. */
|
||||
export interface ProcessResult {
|
||||
@@ -70,13 +90,17 @@ const stopChild = async (
|
||||
return waitForExit(child, SHUTDOWN_GRACE_MS);
|
||||
};
|
||||
|
||||
/** Create a child-process registry for a fixture composition root. */
|
||||
/**
|
||||
* Create a child-process registry for a fixture composition root. A run without
|
||||
* its own `timeoutMs` uses `defaultTimeoutMs`, when supplied.
|
||||
*/
|
||||
export const createTestProcessRegistry = (
|
||||
spawnChild: (
|
||||
command: string,
|
||||
arguments_: readonly string[],
|
||||
options: SpawnOptionsWithoutStdio,
|
||||
) => ChildProcessWithoutNullStreams = nodeSpawn,
|
||||
defaultTimeoutMs?: () => number | undefined,
|
||||
): TestProcesses => {
|
||||
const children = new Set<ChildProcessWithoutNullStreams>();
|
||||
|
||||
@@ -98,7 +122,11 @@ export const createTestProcessRegistry = (
|
||||
return {
|
||||
spawn,
|
||||
run: async (command, arguments_, options = {}) => {
|
||||
const { input, timeoutMs, ...spawnOptions } = options;
|
||||
const {
|
||||
input,
|
||||
timeoutMs = defaultTimeoutMs?.(),
|
||||
...spawnOptions
|
||||
} = options;
|
||||
const child = spawn(command, arguments_, spawnOptions);
|
||||
const stdout: Buffer[] = [];
|
||||
const stderr: Buffer[] = [];
|
||||
@@ -196,11 +224,16 @@ export const createTestProcessRegistry = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Vitest API extended with a bounded child-process registry. */
|
||||
/**
|
||||
* Vitest API extended with a child-process registry whose runs are bounded by
|
||||
* the remaining test deadline unless they choose their own.
|
||||
*/
|
||||
export const processTest = base.extend<{ processes: TestProcesses }>({
|
||||
// oxlint-disable-next-line no-empty-pattern -- Vitest parses fixture dependencies from object destructuring.
|
||||
processes: async ({}, use) => {
|
||||
const processes = createTestProcessRegistry();
|
||||
processes: async ({ task }, use) => {
|
||||
const started = performance.now();
|
||||
const processes = createTestProcessRegistry(nodeSpawn, () =>
|
||||
remainingProcessDeadline(task.timeout, performance.now() - started),
|
||||
);
|
||||
try {
|
||||
await use(processes);
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user