52 lines
1.6 KiB
TypeScript
52 lines
1.6 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { vi } from "vitest";
|
|
|
|
export async function createRebindableDirectoryAlias(params: {
|
|
aliasPath: string;
|
|
targetPath: string;
|
|
}): Promise<void> {
|
|
const aliasPath = path.resolve(params.aliasPath);
|
|
const targetPath = path.resolve(params.targetPath);
|
|
await fs.rm(aliasPath, { recursive: true, force: true });
|
|
await fs.symlink(targetPath, aliasPath, process.platform === "win32" ? "junction" : undefined);
|
|
}
|
|
|
|
export async function withRealpathSymlinkRebindRace<T>(params: {
|
|
shouldFlip: (realpathInput: string) => boolean;
|
|
symlinkPath: string;
|
|
symlinkTarget: string;
|
|
timing?: "before-realpath" | "after-realpath";
|
|
run: () => Promise<T>;
|
|
}): Promise<T> {
|
|
const realRealpath = fs.realpath.bind(fs);
|
|
let flipped = false;
|
|
const realpathSpy = vi
|
|
.spyOn(fs, "realpath")
|
|
.mockImplementation(async (...args: Parameters<typeof fs.realpath>) => {
|
|
const filePath = String(args[0]);
|
|
if (!flipped && params.shouldFlip(filePath)) {
|
|
flipped = true;
|
|
if (params.timing !== "after-realpath") {
|
|
await createRebindableDirectoryAlias({
|
|
aliasPath: params.symlinkPath,
|
|
targetPath: params.symlinkTarget,
|
|
});
|
|
return await realRealpath(...args);
|
|
}
|
|
const resolved = await realRealpath(...args);
|
|
await createRebindableDirectoryAlias({
|
|
aliasPath: params.symlinkPath,
|
|
targetPath: params.symlinkTarget,
|
|
});
|
|
return resolved;
|
|
}
|
|
return await realRealpath(...args);
|
|
});
|
|
try {
|
|
return await params.run();
|
|
} finally {
|
|
realpathSpy.mockRestore();
|
|
}
|
|
}
|