FN-6255: redirect tmpdir mkdtemp calls in tests
Keep test-created temp directories under the Fusion worker root. - Redirect fs.mkdtemp and fs.promises.mkdtemp prefixes rooted at the OS temp dir into per-process worker sinks. - Sweep stale redirect sinks and clean current-process sinks on exit. - Add regression coverage for sync, async, realpath, nested, and Buffer prefix behavior. - Remove the restored merger file-scope invariant test from quarantine. Files changed: packages/core/src/__test-utils__/vitest-setup.ts | 119 +++++++++++++++++++-- .../__tests__/vitest-setup-tmp-redirect.test.ts | 68 ++++++++++++ scripts/lib/test-quarantine.json | 5 - 3 files changed, 181 insertions(+), 11 deletions(-) Fusion-Task-Id: FN-6255 Fusion-Task-Lineage: 4cc855c6-37dd-4dfb-a545-1fd1885779c4
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
import { afterEach, expect } from "vitest";
|
||||
import { createRequire, syncBuiltinESMExports } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { isMainThread } from "node:worker_threads";
|
||||
import { assertOutsideRealFusionPath } from "../test-safety.js";
|
||||
@@ -40,7 +40,16 @@ const requireFromHere = createRequire(import.meta.url);
|
||||
const fs = requireFromHere("node:fs") as FsModule;
|
||||
const fsPromises = requireFromHere("node:fs/promises") as FsPromisesModule;
|
||||
const childProcess = requireFromHere("node:child_process") as ChildProcessModule;
|
||||
const { mkdtempSync, mkdirSync, rmSync, realpathSync, existsSync } = fs;
|
||||
const {
|
||||
appendFileSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
realpathSync,
|
||||
existsSync,
|
||||
writeFileSync,
|
||||
} = fs;
|
||||
|
||||
type EmitWarningArgs = Parameters<typeof process.emitWarning>;
|
||||
type EmitWarningRestArgs = EmitWarningArgs extends [string | Error, ...infer Rest] ? Rest : never;
|
||||
@@ -160,6 +169,102 @@ const WORKER_ROOT = join(tmpdir(), "fusion-test-workers");
|
||||
try { mkdirSync(WORKER_ROOT, { recursive: true }); } catch { /* ignore */ }
|
||||
process.env.FUSION_TEST_WORKER_ROOT = WORKER_ROOT;
|
||||
|
||||
const REAL_TMPDIR = (() => {
|
||||
try {
|
||||
return realpathSync(tmpdir());
|
||||
} catch {
|
||||
return resolve(tmpdir());
|
||||
}
|
||||
})();
|
||||
|
||||
const TMPDIR_REDIRECT_REGISTRY = join(WORKER_ROOT, ".redir-pids");
|
||||
let tmpdirRedirectSink: string | null = null;
|
||||
let tmpdirRedirectExitCleanupInstalled = false;
|
||||
let tmpdirRedirectSweepComplete = false;
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
return code === "EPERM";
|
||||
}
|
||||
}
|
||||
|
||||
function sweepDeadTmpdirRedirectSinks(): void {
|
||||
if (tmpdirRedirectSweepComplete) return;
|
||||
tmpdirRedirectSweepComplete = true;
|
||||
|
||||
let ownerPids: number[];
|
||||
try {
|
||||
ownerPids = Array.from(new Set(
|
||||
readFileSync(TMPDIR_REDIRECT_REGISTRY, "utf8")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => Number.parseInt(line, 10))
|
||||
.filter((pid) => Number.isInteger(pid) && pid > 0),
|
||||
));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const liveOwnerPids: number[] = [];
|
||||
for (const ownerPid of ownerPids) {
|
||||
if (ownerPid === process.pid || isProcessAlive(ownerPid)) {
|
||||
liveOwnerPids.push(ownerPid);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
rmSync(join(WORKER_ROOT, `redir-${ownerPid}`), { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore stale-sink cleanup failures; global teardown still owns WORKER_ROOT.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
writeFileSync(TMPDIR_REDIRECT_REGISTRY, liveOwnerPids.length > 0 ? `${liveOwnerPids.join("\n")}\n` : "");
|
||||
} catch {
|
||||
// Best-effort only; stale entries are harmless and swept by future workers.
|
||||
}
|
||||
}
|
||||
|
||||
function ensureTmpdirRedirectSink(): string {
|
||||
if (tmpdirRedirectSink) return tmpdirRedirectSink;
|
||||
|
||||
sweepDeadTmpdirRedirectSinks();
|
||||
const sink = join(WORKER_ROOT, `redir-${process.pid}`);
|
||||
mkdirSync(sink, { recursive: true });
|
||||
try {
|
||||
appendFileSync(TMPDIR_REDIRECT_REGISTRY, `${process.pid}\n`);
|
||||
} catch {
|
||||
// Best-effort only; the process exit hook and global teardown still clean up.
|
||||
}
|
||||
tmpdirRedirectSink = sink;
|
||||
|
||||
if (!tmpdirRedirectExitCleanupInstalled) {
|
||||
tmpdirRedirectExitCleanupInstalled = true;
|
||||
process.once("exit", () => {
|
||||
try {
|
||||
rmSync(sink, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Best-effort only. vitest globalTeardown also sweeps WORKER_ROOT.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sink;
|
||||
}
|
||||
|
||||
function redirectTmpdirPrefix<T>(prefix: T): T {
|
||||
if (typeof prefix !== "string") return prefix;
|
||||
|
||||
const parent = dirname(prefix);
|
||||
if (parent !== tmpdir() && parent !== REAL_TMPDIR) return prefix;
|
||||
|
||||
return join(ensureTmpdirRedirectSink(), basename(prefix)) as T;
|
||||
}
|
||||
|
||||
function ensureIsolatedHome(): void {
|
||||
const existingHome = process.env.HOME ?? process.env.USERPROFILE;
|
||||
if (existingHome && existingHome.includes(tmpdir()) && existingHome.includes(TEST_HOME_PREFIX)) {
|
||||
@@ -290,8 +395,9 @@ function installFsGuards(): void {
|
||||
return originalFs.cpSync(src, dest, options as Parameters<typeof fs.cpSync>[2]);
|
||||
}) as typeof fs.cpSync;
|
||||
mutableFs.mkdtempSync = ((prefix, options) => {
|
||||
guardOne(prefix, "fs.mkdtempSync");
|
||||
return originalFs.mkdtempSync(prefix, options as Parameters<typeof fs.mkdtempSync>[1]);
|
||||
const redirectedPrefix = redirectTmpdirPrefix(prefix);
|
||||
guardOne(redirectedPrefix, "fs.mkdtempSync");
|
||||
return originalFs.mkdtempSync(redirectedPrefix, options as Parameters<typeof fs.mkdtempSync>[1]);
|
||||
}) as typeof fs.mkdtempSync;
|
||||
mutableFs.openSync = ((path, flags, mode) => {
|
||||
guardOne(path, "fs.openSync");
|
||||
@@ -408,8 +514,9 @@ function installFsGuards(): void {
|
||||
return originalFsPromises.open(...args);
|
||||
}) as typeof fsPromises.open;
|
||||
mutableFsPromises.mkdtemp = (async (...args: Parameters<typeof fsPromises.mkdtemp>) => {
|
||||
guardOne(args[0], "fs.promises.mkdtemp");
|
||||
return originalFsPromises.mkdtemp(...args);
|
||||
const redirectedPrefix = redirectTmpdirPrefix(args[0]);
|
||||
guardOne(redirectedPrefix, "fs.promises.mkdtemp");
|
||||
return originalFsPromises.mkdtemp(redirectedPrefix, args[1]);
|
||||
}) as typeof fsPromises.mkdtemp;
|
||||
mutableFsPromises.truncate = (async (...args: Parameters<typeof fsPromises.truncate>) => {
|
||||
guardOne(args[0], "fs.promises.truncate");
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, realpathSync } from "node:fs";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, sep } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
const createdPaths: string[] = [];
|
||||
|
||||
function remember(path: string): string {
|
||||
createdPaths.push(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
function expectUnderWorkerRoot(path: string): void {
|
||||
const workerRoot = process.env.FUSION_TEST_WORKER_ROOT;
|
||||
expect(workerRoot).toBeTruthy();
|
||||
expect(path.startsWith(`${workerRoot}${sep}`)).toBe(true);
|
||||
expect(dirname(path)).toBe(join(workerRoot!, `redir-${process.pid}`));
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const path of createdPaths.splice(0).reverse()) {
|
||||
rmSync(path, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("vitest setup tmpdir mkdtemp redirect", () => {
|
||||
it("redirects sync mkdtemp prefixes rooted directly at the OS temp dir", () => {
|
||||
const path = remember(mkdtempSync(join(tmpdir(), "fn-redirect-sync-")));
|
||||
|
||||
expectUnderWorkerRoot(path);
|
||||
});
|
||||
|
||||
it("redirects async mkdtemp prefixes rooted directly at the OS temp dir", async () => {
|
||||
const path = remember(await mkdtemp(join(tmpdir(), "fn-redirect-async-")));
|
||||
|
||||
expectUnderWorkerRoot(path);
|
||||
});
|
||||
|
||||
it("redirects the realpath spelling of the OS temp dir when it differs", () => {
|
||||
const realTmpdir = realpathSync(tmpdir());
|
||||
if (realTmpdir === tmpdir()) {
|
||||
expect(realTmpdir).toBe(tmpdir());
|
||||
return;
|
||||
}
|
||||
|
||||
const path = remember(mkdtempSync(join(realTmpdir, "fn-redirect-realpath-")));
|
||||
|
||||
expectUnderWorkerRoot(path);
|
||||
});
|
||||
|
||||
it("leaves nested temp-root prefixes unchanged", () => {
|
||||
const parent = remember(join(tmpdir(), `fn-redirect-parent-${process.pid}-${Date.now()}`));
|
||||
mkdirSync(parent, { recursive: true });
|
||||
|
||||
const path = remember(mkdtempSync(join(parent, "nested-")));
|
||||
|
||||
expect(path.startsWith(`${parent}${sep}`)).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves non-string prefixes untouched", () => {
|
||||
const prefix = Buffer.from(join(tmpdir(), "fn-redirect-buffer-"));
|
||||
|
||||
const path = remember(mkdtempSync(prefix));
|
||||
|
||||
expect(path.startsWith(`${tmpdir()}${sep}`)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user