import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { mkdir, rm } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { ALL_TARGETS, archiveName, checksumName, computeChecksum, execName, packageTarget, parseTargets, } from "../../scripts/release"; describe("parseTargets", () => { test("默认返回全部目标", () => { const targets = parseTargets([]); expect(targets).toEqual(ALL_TARGETS); }); test("解析单一目标", () => { const targets = parseTargets(["--target", "linux-x64"]); expect(targets).toHaveLength(1); expect(targets[0]!.bunTarget).toBe("bun-linux-x64"); expect(targets[0]!.os).toBe("linux"); expect(targets[0]!.arch).toBe("x64"); }); test("解析多个逗号分隔目标", () => { const targets = parseTargets(["--target", "linux-x64,darwin-arm64,windows-x64"]); expect(targets).toHaveLength(3); expect(targets[0]!.bunTarget).toBe("bun-linux-x64"); expect(targets[1]!.bunTarget).toBe("bun-darwin-arm64"); expect(targets[2]!.bunTarget).toBe("bun-windows-x64"); }); test("解析 musl 变体", () => { const targets = parseTargets(["--target", "linux-x64-musl"]); expect(targets).toHaveLength(1); expect(targets[0]!.bunTarget).toBe("bun-linux-x64-musl"); expect(targets[0]!.os).toBe("linux"); expect(targets[0]!.arch).toBe("x64-musl"); }); test("无效 target 导致进程退出", () => { const exitCalls: number[] = []; // eslint-disable-next-line @typescript-eslint/unbound-method const originalExit = process.exit; process.exit = ((code: number) => { exitCalls.push(code); }) as never; try { parseTargets(["--target", "invalid-target"]); } finally { process.exit = originalExit; } expect(exitCalls).toEqual([1]); }); }); describe("execName", () => { const linuxTarget = ALL_TARGETS.find((t) => t.bunTarget === "bun-linux-x64")!; const windowsTarget = ALL_TARGETS.find((t) => t.bunTarget === "bun-windows-x64")!; const muslTarget = ALL_TARGETS.find((t) => t.bunTarget === "bun-linux-x64-musl")!; test("Linux x64 可执行文件命名", () => { expect(execName(linuxTarget, "0.1.0")).toBe("dial-server-0.1.0-linux-x64"); }); test("Windows 可执行文件命名带 .exe 后缀", () => { expect(execName(windowsTarget, "0.1.0")).toBe("dial-server-0.1.0-windows-x64.exe"); }); test("musl 变体命名", () => { expect(execName(muslTarget, "0.1.0")).toBe("dial-server-0.1.0-linux-x64-musl"); }); test("版本号正确嵌入", () => { expect(execName(linuxTarget, "1.2.3")).toBe("dial-server-1.2.3-linux-x64"); }); }); describe("archiveName", () => { const linuxTarget = ALL_TARGETS.find((t) => t.bunTarget === "bun-linux-x64")!; const windowsTarget = ALL_TARGETS.find((t) => t.bunTarget === "bun-windows-x64")!; test("Linux 压缩包命名", () => { expect(archiveName(linuxTarget, "0.1.0")).toBe("dial-server_0.1.0_linux_x64.tar.gz"); }); test("Windows 压缩包命名", () => { expect(archiveName(windowsTarget, "0.1.0")).toBe("dial-server_0.1.0_windows_x64.tar.gz"); }); }); describe("checksumName", () => { const linuxTarget = ALL_TARGETS.find((t) => t.bunTarget === "bun-linux-x64")!; test("校验和文件命名", () => { expect(checksumName(linuxTarget, "0.1.0")).toBe("dial-server_0.1.0_linux_x64.tar.gz.sha256"); }); }); describe("computeChecksum", () => { const projectRoot = fileURLToPath(new URL("../../", import.meta.url)); const packagesDir = join(projectRoot, "dist/release/packages"); beforeEach(async () => { await mkdir(packagesDir, { recursive: true }); }); afterEach(async () => { await rm(packagesDir, { force: true, recursive: true }); }); test("生成正确格式的 sha256 文件", async () => { const archivePath = join(packagesDir, "dial-server_0.1.0_linux_x64.tar.gz"); const content = Buffer.from("test archive content"); await Bun.write(archivePath, content); const checksumPath = await computeChecksum(archivePath); const checksumContent = await Bun.file(checksumPath).text(); const parts = checksumContent.split(" "); expect(parts).toHaveLength(2); expect(parts[0]).toMatch(/^[0-9a-f]{64}$/); expect(parts[1]).toBe("dial-server_0.1.0_linux_x64.tar.gz\n"); }); test("校验和与文件内容一致", async () => { const archivePath = join(packagesDir, "test.tar.gz"); const content = Buffer.from("hello world"); await Bun.write(archivePath, content); const checksumPath = await computeChecksum(archivePath); const checksumContent = await Bun.file(checksumPath).text(); const hash = checksumContent.split(" ")[0]!; const expectedHash = Bun.CryptoHasher.hash("sha256", content, "hex"); expect(hash).toBe(expectedHash); }); }); describe("packageTarget", () => { const projectRoot = fileURLToPath(new URL("../../", import.meta.url)); const packagesDir = join(projectRoot, "dist/release/packages"); beforeEach(async () => { await mkdir(packagesDir, { recursive: true }); }); afterEach(async () => { await rm(packagesDir, { force: true, recursive: true }); }); test("Linux 压缩包包含正确文件、目录前缀和权限位", async () => { const linuxTarget = ALL_TARGETS.find((t) => t.bunTarget === "bun-linux-x64")!; const binaryPath = join(packagesDir, "test-binary"); await Bun.write(binaryPath, "binary content"); const archivePath = await packageTarget(linuxTarget, "0.1.0", binaryPath); const archiveContent = await Bun.file(archivePath).arrayBuffer(); const tar = await import("tar-stream"); const zlib = await import("node:zlib"); const extract = tar.extract(); const entries: Array<{ mode: number; name: string }> = []; await new Promise((resolve, reject) => { const gunzip = zlib.createGunzip(); gunzip.write(Buffer.from(archiveContent)); gunzip.end(); gunzip.pipe(extract); extract.on("entry", (header: { mode?: number; name: string }, _stream: unknown, next: () => void) => { entries.push({ mode: header.mode ?? 0, name: header.name }); next(); }); extract.on("finish", resolve); extract.on("error", reject); }); const names = entries.map((e) => e.name); expect(names).toContain("dial-server_0.1.0_linux_x64/dial-server"); expect(names).toContain("dial-server_0.1.0_linux_x64/probes.example.yaml"); expect(names).toContain("dial-server_0.1.0_linux_x64/LICENSE"); const binaryEntry = entries.find((e) => e.name.endsWith("/dial-server"))!; expect(binaryEntry.mode).toBe(0o755); const probesEntry = entries.find((e) => e.name.endsWith("probes.example.yaml"))!; expect(probesEntry.mode).toBe(0o644); }); test("Windows 压缩包内可执行文件名为 dial-server.exe", async () => { const windowsTarget = ALL_TARGETS.find((t) => t.bunTarget === "bun-windows-x64")!; const binaryPath = join(packagesDir, "test-binary.exe"); await Bun.write(binaryPath, "binary content"); const archivePath = await packageTarget(windowsTarget, "0.1.0", binaryPath); const archiveContent = await Bun.file(archivePath).arrayBuffer(); const tar = await import("tar-stream"); const zlib = await import("node:zlib"); const extract = tar.extract(); const names: string[] = []; await new Promise((resolve, reject) => { const gunzip = zlib.createGunzip(); gunzip.write(Buffer.from(archiveContent)); gunzip.end(); gunzip.pipe(extract); extract.on("entry", (header: { name: string }, _stream: unknown, next: () => void) => { names.push(header.name); next(); }); extract.on("finish", resolve); extract.on("error", reject); }); expect(names).toContain("dial-server_0.1.0_windows_x64/dial-server.exe"); }); });