Files
Alfred/tests/helpers.ts

184 lines
4.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Database from "bun:sqlite";
import { randomUUID } from "node:crypto";
import { mkdirSync, rmSync } from "node:fs";
import { rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { MigrationRecord } from "../src/server/db/load-migrations";
import { createDatabase } from "../src/server/db/connection";
import { loadMigrationsFromDir } from "../src/server/db/load-migrations";
import { runMigrations } from "../src/server/db/migrate";
import { createMemoryLogger } from "../src/server/logger";
const RETRYABLE_RM_CODES = new Set(["EBUSY", "ENOTEMPTY", "EPERM"]);
export interface TestDatabaseHandle {
cleanup: () => void;
close: () => void;
db: Database;
dir: string;
logger: ReturnType<typeof createMemoryLogger>;
}
export function closeSqliteForTest(db: Database): void {
try {
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
} catch {
// 关闭前的 checkpoint 是尽力操作;失败时仍继续 close避免掩盖原测试断言。
}
try {
db.exec("PRAGMA journal_mode = DELETE");
} catch {
// Windows 上 WAL/SHM 句柄释放可能延迟,切回 DELETE 失败时仍继续关闭连接。
}
db.close();
forceBunGc();
sleepSync(100);
}
export function configureSqliteForTest(db: Database): void {
// 生产库使用 WAL测试库切回 DELETE避免 Windows 上 WAL/SHM 句柄延迟导致清理 EBUSY。
db.exec("PRAGMA journal_mode = DELETE");
}
export function createMigratedMemoryTestDatabase(prefix: string): TestDatabaseHandle {
const dir = makeTempDir(prefix);
const logger = createMemoryLogger();
const db = new Database(":memory:");
configureSqliteForTest(db);
let closed = false;
runMigrations(db, loadMigrationsFromDir(), dir, logger);
const close = () => {
if (closed) return;
closeSqliteForTest(db);
closed = true;
};
return {
cleanup: () => {
close();
rmRetrySync(dir);
},
close,
db,
dir,
logger,
};
}
export function createMigratedTestDatabase(prefix: string): TestDatabaseHandle {
return createTestDatabase(prefix, loadMigrationsFromDir());
}
export function createTestDatabase(prefix: string, migrations: MigrationRecord[] = []): TestDatabaseHandle {
const dir = makeTempDir(prefix);
const logger = createMemoryLogger();
const db = createDatabase(dir, logger);
configureSqliteForTest(db);
let closed = false;
if (migrations.length > 0) {
runMigrations(db, migrations, dir, logger);
}
const close = () => {
if (closed) return;
closeSqliteForTest(db);
closed = true;
};
return {
cleanup: () => {
close();
rmRetrySync(dir);
},
close,
db,
dir,
logger,
};
}
export function makeTempDir(prefix: string): string {
const dir = join(tmpdir(), `${prefix}-${randomUUID()}`);
mkdirSync(dir, { recursive: true });
return dir;
}
export function openTestDatabase(dir: string): Pick<TestDatabaseHandle, "close" | "db" | "logger"> {
const logger = createMemoryLogger();
const db = createDatabase(dir, logger);
configureSqliteForTest(db);
let closed = false;
return {
close: () => {
if (closed) return;
closeSqliteForTest(db);
closed = true;
},
db,
logger,
};
}
export async function rmRetry(dir: string, retries = 10, delayMs = 500) {
for (let i = 0; i < retries; i++) {
try {
await rm(dir, { force: true, recursive: true });
return;
} catch (e) {
const code = getErrorCode(e);
if (i === retries - 1 || (code && !RETRYABLE_RM_CODES.has(code))) {
throw withCleanupMessage(e, dir, i + 1);
}
await new Promise((r) => setTimeout(r, delayMs));
}
}
}
export function rmRetrySync(dir: string, retries = 300, delayMs = 50): void {
for (let i = 0; i < retries; i++) {
try {
forceBunGc();
rmSync(dir, { force: true, recursive: true });
return;
} catch (e) {
const code = getErrorCode(e);
if (i === retries - 1 || (code && !RETRYABLE_RM_CODES.has(code))) {
throw withCleanupMessage(e, dir, i + 1);
}
sleepSync(delayMs);
}
}
}
function forceBunGc(): void {
const bun = (globalThis as typeof globalThis & { Bun?: { gc?: (force?: boolean) => void } }).Bun;
bun?.gc?.(true);
}
function getErrorCode(error: unknown): string | undefined {
return typeof error === "object" && error !== null && "code" in error
? String((error as { code?: unknown }).code)
: undefined;
}
function sleepSync(ms: number): void {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
function withCleanupMessage(error: unknown, dir: string, attempts: number): Error {
if (!(error instanceof Error)) {
return new Error(`测试临时目录清理失败:${dir},已重试 ${attempts} 次,原始错误:${String(error)}`);
}
error.message = `测试临时目录清理失败:${dir},已重试 ${attempts} 次。${error.message}`;
return error;
}