feat: 添加 retryRm 工具函数及测试
This commit is contained in:
119
tests/helpers/cleanup.test.ts
Normal file
119
tests/helpers/cleanup.test.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
|
||||||
|
import { mkdir, access, rm } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const TMP_DIR = join(import.meta.dir, "__tmp_cleanup_test__");
|
||||||
|
|
||||||
|
async function dirExists(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await access(path);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("retryRm", () => {
|
||||||
|
describe("正常删除", () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
if (await dirExists(TMP_DIR)) {
|
||||||
|
await rm(TMP_DIR, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("删除存在的目录", async () => {
|
||||||
|
const { retryRm } = await import("./cleanup.ts");
|
||||||
|
await mkdir(TMP_DIR, { recursive: true });
|
||||||
|
await retryRm(TMP_DIR);
|
||||||
|
expect(await dirExists(TMP_DIR)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("删除不存在的路径不报错(force:true)", async () => {
|
||||||
|
const { retryRm } = await import("./cleanup.ts");
|
||||||
|
const nonExist = join(import.meta.dir, "__non_exist__");
|
||||||
|
await expect(retryRm(nonExist)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("EBUSY 重试后成功", () => {
|
||||||
|
let callCount: number;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
callCount = 0;
|
||||||
|
mock.module("node:fs/promises", () => ({
|
||||||
|
rm: mock(async () => {
|
||||||
|
callCount++;
|
||||||
|
if (callCount === 1) {
|
||||||
|
const err: any = new Error("EBUSY");
|
||||||
|
err.code = "EBUSY";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("EBUSY 后重试成功", async () => {
|
||||||
|
const { retryRm } = await import("./cleanup.ts");
|
||||||
|
await retryRm("/some/path", { maxRetries: 3, baseDelay: 10 });
|
||||||
|
expect(callCount).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("重试耗尽后抛出错误", () => {
|
||||||
|
let callCount: number;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
callCount = 0;
|
||||||
|
mock.module("node:fs/promises", () => ({
|
||||||
|
rm: mock(async () => {
|
||||||
|
callCount++;
|
||||||
|
const err: any = new Error("EBUSY");
|
||||||
|
err.code = "EBUSY";
|
||||||
|
throw err;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maxRetries 耗尽后抛出最后一个错误", async () => {
|
||||||
|
const { retryRm } = await import("./cleanup.ts");
|
||||||
|
await expect(retryRm("/some/path", { maxRetries: 2, baseDelay: 10 })).rejects.toThrow(
|
||||||
|
"EBUSY",
|
||||||
|
);
|
||||||
|
expect(callCount).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("非 EBUSY/EPERM 错误不重试", () => {
|
||||||
|
let callCount: number;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
callCount = 0;
|
||||||
|
mock.module("node:fs/promises", () => ({
|
||||||
|
rm: mock(async () => {
|
||||||
|
callCount++;
|
||||||
|
const err: any = new Error("ENOENT: no such file or directory");
|
||||||
|
err.code = "ENOENT";
|
||||||
|
throw err;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ENOENT 错误直接抛出不重试", async () => {
|
||||||
|
const { retryRm } = await import("./cleanup.ts");
|
||||||
|
await expect(retryRm("/some/path")).rejects.toThrow("ENOENT");
|
||||||
|
expect(callCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
21
tests/helpers/cleanup.ts
Normal file
21
tests/helpers/cleanup.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { rm } from "node:fs/promises";
|
||||||
|
|
||||||
|
const RETRY_CODES = new Set(["EBUSY", "EPERM"]);
|
||||||
|
|
||||||
|
export async function retryRm(
|
||||||
|
path: string,
|
||||||
|
{ maxRetries = 5, baseDelay = 100 }: { maxRetries?: number; baseDelay?: number } = {},
|
||||||
|
): Promise<void> {
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
await rm(path, { recursive: true, force: true });
|
||||||
|
return;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (!RETRY_CODES.has(err.code) || attempt === maxRetries) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const delay = baseDelay * 2 ** attempt;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user